Merge "Allow plugins to reference groups in project config"
diff --git a/.buckversion b/.buckversion
index 46408a5..ab18d5d 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-8204fddf60b25a3c2090f3ef0742fca5d466d562
+7e153d4a69044d059288d353fc1a442e07cbea58
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index dbfdf4c..68976e7 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -107,6 +107,7 @@
 org.eclipse.jdt.core.compiler.source=1.7
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
@@ -117,15 +118,18 @@
 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
 org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
 org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
 org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
 org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
 org.eclipse.jdt.core.formatter.blank_lines_after_package=1
 org.eclipse.jdt.core.formatter.blank_lines_before_field=0
@@ -145,6 +149,7 @@
 org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
@@ -161,10 +166,16 @@
 org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
 org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
 org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
 org.eclipse.jdt.core.formatter.compact_else_if=true
 org.eclipse.jdt.core.formatter.continuation_indentation=2
 org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
 org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
 org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
 org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
 org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
@@ -176,10 +187,16 @@
 org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
 org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
 org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
 org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
 org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
 org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
@@ -227,6 +244,7 @@
 org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
 org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
 org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
@@ -245,12 +263,14 @@
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
 org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
 org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
 org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
@@ -274,6 +294,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
 org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
@@ -301,6 +322,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
@@ -329,6 +351,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
 org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
 org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
@@ -338,6 +361,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
 org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
@@ -361,5 +385,9 @@
 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
 org.eclipse.jdt.core.formatter.tabulation.char=space
 org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_on_off_tags=false
 org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
 org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
+org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
index 7397758..d990610 100644
--- a/.settings/org.eclipse.jdt.ui.prefs
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -1,7 +1,7 @@
 eclipse.preferences.version=1
 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
 formatter_profile=_Google Format
-formatter_settings_version=11
+formatter_settings_version=12
 org.eclipse.jdt.ui.ignorelowercasenames=true
 org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax;
 org.eclipse.jdt.ui.ondemandthreshold=99
diff --git a/BUCK b/BUCK
index 4dd69c3..82f1d72 100644
--- a/BUCK
+++ b/BUCK
@@ -9,6 +9,9 @@
 gerrit_war(name = 'release',  ui = 'ui_optdbg_r', docs = True, context = ['//plugins:core'],  visibility = ['//tools/maven:'])
 
 API_DEPS = [
+  '//gerrit-acceptance-framework:acceptance-framework',
+  '//gerrit-acceptance-framework:acceptance-framework-src',
+  '//gerrit-acceptance-framework:acceptance-framework-javadoc',
   '//gerrit-extension-api:extension-api',
   '//gerrit-extension-api:extension-api-src',
   '//gerrit-extension-api:extension-api-javadoc',
@@ -22,11 +25,9 @@
 
 genrule(
   name = 'api',
-  cmd = ';'.join(
-    ['cd $TMP'] +
-    ['ln -s $(location %s) .' % n for n in API_DEPS] +
-    ['zip -q0 $OUT *']),
-  out = 'api.zip',
+  cmd = 'echo done >$OUT',
+  deps = API_DEPS,
+  out = '__fake.api__',
 )
 
 genrule(
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index c648725..e7b7cdc 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -237,6 +237,13 @@
 +
 Default is -1, permitting infinite time between authentications.
 
+[[auth.registerEmailPrivateKey]]auth.registerEmailPrivateKey::
++
+Private key to use when generating an email verification token.
++
+If not set, a random key is generated when running the
+link:pgm-init.html[site initialization].
+
 [[auth.maxRegisterEmailTokenAge]]auth.maxRegisterEmailTokenAge::
 +
 Time in seconds before an email verification token sent to a user in
@@ -1713,6 +1720,16 @@
 `/Documentation/index.html` can be reached by the browser at app load
 time.
 
+[[gerrit.editGpgKeys]]gerrit.editGpgKeys::
++
+If enabled and server-side signed push validation is also
+link:#receive.enableSignedPush[enabled], enable the
+link:rest-api-accounts.html#list-gpg-keys[REST API endpoints] and web UI
+for editing GPG keys. If disabled, GPG keys can only be added by
+administrators with direct git access to All-Users.
++
+Defaults to true.
+
 [[gerrit.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
 +
 Optional command to install the `commit-msg` hook. Typically of the
@@ -2346,6 +2363,15 @@
 +
 Defaults to no limit.
 
+[[index.maxTerms]]index.maxTerms::
++
+Maximum number of leaf terms to allow in a query. Too-large queries may
+perform poorly, so setting this option causes query parsing to fail fast
+before attempting to send them to the secondary index. Set to 0 for no
+limit.
++
+Defaults to 500.
+
 ==== Lucene configuration
 
 Open and closed changes are indexed in separate indexes named
@@ -2982,6 +3008,14 @@
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
 
+[[receive.maxTrustDepth]]receive.maxTrustDepth::
++
+If signed push validation is link:#receive.enableSignedPush[enabled],
+set to the maximum depth to search when checking if a key is
+link:#receive.trustedKey[trusted].
++
+Default is 0, meaning only explicitly trusted keys are allowed.
+
 [[receive.threadPoolSize]]receive.threadPoolSize::
 +
 Maximum size of the thread pool in which the change data in received packs is
@@ -3000,6 +3034,25 @@
 Default is 2 minutes. If no unit is specified, milliseconds
 is assumed.
 
+[[receive.trustedKey]]receive.trustedKey::
++
+List of GPG key fingerprints that should be considered trust roots by
+the server when signed push validation is
+link:#receive.enableSignedPush[enabled]. A key is trusted by the server
+if it is either in this list, or a path of trust signatures leads from
+the key to a configured trust root. The maximum length of the path is
+determined by link:#receive.maxTrustDepth[`receive.maxTrustDepth`].
++
+Key fingerprints can be displayed with `gpg --list-keys
+--with-fingerprint`.
++
+Trust signatures can be added to a key using the `tsign` command to
+link:https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html[
+`gpg --edit-key`], after which the signed key should be re-uploaded.
++
+If no keys are specified, web-of-trust checks are disabled. This is the
+default behavior.
+
 
 [[repository]]
 === Section repository
@@ -3861,7 +3914,6 @@
 ----
 [auth]
   registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6
-  restTokenPrivateKey = 7e40PzCjlUKOnXATvcBNXH6oyiu+r0dFk2c=
 
 [database]
   username = webuser
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 0d3ff58..7d03681 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -164,6 +164,17 @@
 Default is `INHERIT`, which means that this property is inherited from
 the parent project.
 
+[[receive.requireSignedPush]]receive.requireSignedPush::
++
+Controls whether server-side signed push validation is required on the
+project. Only has an effect if signed push validation is enabled on the
+server, and link:#receive.enableSignedPush is set on the project. See
+the link:config-gerrit.html#receive.enableSignedPush[global
+configuration] for details.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index b3b72c4..f19f377 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -18,6 +18,20 @@
 
 If this option interests you, you might want to consider link:install-quick.html[the quick guide].
 
+[[createdb_derby]]
+=== Apache Derby
+
+If Derby is selected, Gerrit will automatically set up the embedded Derby
+database as backend so no set up or configuration is necessary.
+
+Currently only support for embedded mode is added. There are two other
+deployment options for Apache Derby that can be added later [1]:
++
+* Derby Network Server (standalone mode)
+* Embedded Server (hybrid mode)
++
+[1] http://db.apache.org/derby/papers/DerbyTut/ns_intro.html#ns
+
 [[createdb_postgres]]
 === PostgreSQL
 
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 49a4d85..7d30a91 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -106,7 +106,7 @@
 The output executable WAR will be placed in:
 
 ----
-  buck-out/gen/gerrit.war
+  buck-out/gen/gerrit/gerrit.war
 ----
 
 
@@ -121,7 +121,7 @@
 The output executable WAR will be placed in:
 
 ----
-  buck-out/gen/headless.war
+  buck-out/gen/headless/headless.war
 ----
 
 === Extension and Plugin API JAR Files
@@ -137,8 +137,8 @@
 
 ----
   buck-out/gen/gerrit-plugin-api/plugin-api.jar
+  buck-out/gen/gerrit-plugin-api/plugin-api-javadoc/plugin-api-javadoc.jar
   buck-out/gen/gerrit-plugin-api/plugin-api-src.jar
-  buck-out/gen/gerrit-plugin-api/plugin-api-javadoc.jar
 ----
 
 Install {extension,plugin,gwt}-api to the local maven repository:
@@ -170,7 +170,7 @@
 The JAR files will also be packaged in:
 
 ----
-  buck-out/gen/plugins/core.zip
+  buck-out/gen/plugins/core/core.zip
 ----
 
 To build a specific plugin:
@@ -224,7 +224,7 @@
 The html files will also be bundled into `searchfree.zip` in this location:
 
 ----
-  buck-out/gen/Documentation/searchfree.zip
+  buck-out/gen/Documentation/searchfree/searchfree.zip
 ----
 
 To build the executable WAR with the documentation included:
@@ -236,7 +236,7 @@
 The WAR file will be placed in:
 
 ----
-  buck-out/gen/withdocs.war
+  buck-out/gen/withdocs/withdocs.war
 ----
 
 [[soyc]]
@@ -272,7 +272,7 @@
 The output release WAR will be placed in:
 
 ----
-  buck-out/gen/release.war
+  buck-out/gen/release/release.war
 ----
 
 [[all]]
@@ -318,11 +318,10 @@
 * ssh
 * slow
 
-To run a specific test, e.g. the acceptance test
-`com.google.gerrit.acceptance.git.HttpPushForReviewIT`:
+To run a specific test group, e.g. the rest-account test group:
 
 ----
-  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:HttpPushForReviewIT
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account
 ----
 
 To create test coverage report:
@@ -605,29 +604,41 @@
 needs to be repeated, the unit test cache for that test must be removed first:
 
 ----
-  rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/.AddRemoveGroupMembersIT/
+  rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/.rest-account/
 ----
 
 After clearing the cache, the test can be run again:
 
 ----
-  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/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
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account
+  [-] TESTING...FINISHED 12,3s (12 PASS/0 FAIL)
+  RESULTS FOR //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account
+  PASS     970ms  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.CapabilitiesIT
+  PASS     999ms  1 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.EditPreferencesIT
+  PASS      1,2s  1 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.GetAccountDetailIT
+  PASS     951ms  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.GetAccountIT
+  PASS      6,4s  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.GetDiffPreferencesIT
+  PASS      1,2s  4 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.PutUsernameIT
   TESTS PASSED
 ----
 
 An alternative approach is to use Buck's `--filters` (`-f`) option:
 
 ----
-  buck test -f 'com.google.gerrit.acceptance.rest.change.SubmitByMergeAlwaysIT'
-  TESTING SELECTED TESTS
-  PASS  14,5s  6 Passed   0 Failed   com.google.gerrit.acceptance.rest.change.SubmitByMergeAlwaysIT
+  buck test -f 'com.google.gerrit.acceptance.rest.account.CapabilitiesIT'
+  Using buckd.
+  [-] PROCESSING BUCK FILES...FINISHED 1,0s [100%]
+  [-] BUILDING...FINISHED 2,8s [100%] (334/701 JOBS, 110 UPDATED, 5,1% CACHE MISS)
+  [-] TESTING...FINISHED 9,2s (6 PASS/0 FAIL)
+  RESULTS FOR SELECTED TESTS
+  PASS      8,0s  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.CapabilitiesIT
+  PASS    <100ms  4 Passed   0 Skipped   0 Failed   //tools:util_test
   TESTS PASSED
 ----
 
 When this option is used, the cache is disabled per design and doesn't need to
-be explicitly deleted.
+be explicitly deleted. Note, that this is a known issue, that python tests are
+always executed.
 
 Note that when this option is used, the whole unit test cache is dropped, so
 repeating the
@@ -645,6 +656,24 @@
 buck test --no-results-cache
 ----
 
+== Upgrading Buck
+
+The following tests should be executed, when Buck version is upgraded:
+
+* buck build release
+* buck build api_install
+* buck test
+* buck build gerrit, change some sources in gerrit-server project,
+  repeat buck build gerrit and verify that gerrit.war was updated
+* install and verify new gerrit site
+* upgrade and verify existing gerrit site
+* reindex existing gerrit site
+* verify that tools/eclipse/project.py produces sane Eclipse project
+* verify that tools/eclipse/project.py --src generates sources as well
+* verify that unit test execution from Eclipse works
+* verify that daemon started from Eclipse works
+* verify that GWT SDM debug session started from Eclipse works
+
 == Known issues and bugs
 
 === Symbolic links and `watchman`
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index b64973a..4959ced 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -87,7 +87,7 @@
 testing site for development use:
 
 ----
-  java -jar buck-out/gen/gerrit.war init -d ../gerrit_testsite
+  java -jar buck-out/gen/gerrit/gerrit.war init -d ../gerrit_testsite
 ----
 
 Accept defaults by pressing Enter until 'init' completes, or add
@@ -130,7 +130,7 @@
 copying to the test site:
 
 ----
-  java -jar buck-out/gen/gerrit.war daemon -d ../gerrit_testsite
+  java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite
 ----
 
 === Running the Daemon with Gerrit Inspector
@@ -149,7 +149,7 @@
 command used to launch the daemon:
 
 ----
-  java -jar buck-out/gen/gerrit.war daemon -d ../gerrit_testsite -s
+  java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite -s
 ----
 
 Gerrit Inspector examines Java libraries first, then loads
@@ -176,7 +176,7 @@
 command line.  If the daemon is not currently running:
 
 ----
-  java -jar buck-out/gen/gerrit.war gsql -d ../gerrit_testsite
+  java -jar buck-out/gen/gerrit/gerrit.war gsql -d ../gerrit_testsite
 ----
 
 Or, if it is running and the database is in use, connect over SSH
diff --git a/Documentation/doc.css.in b/Documentation/doc.css.in
index 6be89f6..429e81c 100644
--- a/Documentation/doc.css.in
+++ b/Documentation/doc.css.in
@@ -17,6 +17,10 @@
   border-bottom: 2px solid silver;
 }
 
+h1 {
+  margin-top: 1.5em;
+}
+
 p {
   margin: 0.5em 0 0.5em 0;
 }
diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt
index 7bfbfe1..a5c805c 100644
--- a/Documentation/error-no-new-changes.txt
+++ b/Documentation/error-no-new-changes.txt
@@ -45,6 +45,15 @@
 it with a new Change-Id (case 1. and 3. above), otherwise the push
 will fail with another error message.
 
+== Fast-forward merges
+
+You will also encounter this error if you did a Fast-forward merge
+and try to push the result.  A workaround is to use the
+link:user-upload.html#base[Selecting Merge Base]
+feature or enable the
+link:project-configuration.html#_use_target_branch_when_determining_new_changes_to_open[
+Use target branch when determining new changes to open]
+configuration.
 
 GERRIT
 ------
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index 4a64892..ffd68e7 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -163,7 +163,7 @@
 Download a local clone of the repository and move into it
 
 ----
-  user@host:~$ git clone ssh://user@host:29418/demo-project
+  user@host:~$ git clone ssh://user@localhost:29418/demo-project
   Cloning into demo-project...
   remote: Counting objects: 2, done
   remote: Finding sources: 100% (2/2)
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
index b1116d3..e1d8e8b 100644
--- a/Documentation/pgm-reindex.txt
+++ b/Documentation/pgm-reindex.txt
@@ -15,15 +15,6 @@
 --threads::
 	Number of threads to use for indexing.
 
---recheck-mergeable::
-	Recheck the mergeable flag on all open changes. For each open change,
-	look up for which commit the mergeability check was last done and if
-	this commit is different from the HEAD commit of the change's destination
-	branch, recompute the mergeability flag of the change by checking if the
-	commit of the current patch set can be merged into the destination branch.
-	Because this operation is computationally expensive, it is not enabled
-	by default.
-
 --schema-version::
 	Schema version to reindex; default is most recent version.
 
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index fb54601..a320a54 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -713,7 +713,9 @@
       "user_ids": [
         "John Doe \u003cjohn.doe@example.com\u003e"
       ],
-      "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0"
+      "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0",
+      "status": "TRUSTED",
+      "problems": [],
     },
   }
 ----
@@ -747,7 +749,9 @@
     "user_ids": [
       "John Doe \u003cjohn.doe@example.com\u003e"
     ],
-    "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0"
+    "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0",
+    "status": "TRUSTED",
+    "problems": [],
   }
 ----
 
@@ -797,6 +801,8 @@
         "John Doe \u003cjohn.doe@example.com\u003e"
       ],
       "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0"
+      "status": "TRUSTED",
+      "problems": [],
     }
     "DEADBEEF": {}
   }
@@ -1314,6 +1320,82 @@
   }
 ----
 
+[[get-edit-preferences]]
+=== Get Edit Preferences
+--
+'GET /accounts/link:#account-id[\{account-id\}]/preferences.edit'
+--
+
+Retrieves the edit preferences of a user.
+
+.Request
+----
+  GET /a/accounts/self/preferences.edit HTTP/1.0
+----
+
+As result the edit preferences of the user are returned as a
+link:#edit-preferences-info[EditPreferencesInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "theme": "ECLIPSE",
+    "key_map_type": "VIM",
+    "tab_size": 4,
+    "line_length": 80,
+    "cursor_blink_rate": 530,
+    "hide_top_menu": true,
+    "show_whitespace_errors": true,
+    "hide_line_numbers": true,
+    "match_brackets": true,
+    "auto_close_brackets": true
+  }
+----
+
+[[set-edit-preferences]]
+=== Set Edit Preferences
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/preferences.edit'
+--
+
+Sets the edit preferences of a user.
+
+The new edit preferences must be provided in the request body as a
+link:#edit-preferences-info[EditPreferencesInfo] entity.
+
+.Request
+----
+  PUT /a/accounts/self/preferences.edit HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "theme": "ECLIPSE",
+    "key_map_type": "VIM",
+    "tab_size": 4,
+    "line_length": 80,
+    "cursor_blink_rate": 530,
+    "hide_top_menu": true,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "hide_line_numbers": true,
+    "match_brackets": true,
+    "auto_close_brackets": true
+  }
+----
+
+The response is "`204 No Content`"
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[get-starred-changes]]
 === Get Starred Changes
 --
@@ -1611,7 +1693,7 @@
 |`syntax_highlighting`         |not set if `false`|
 Whether syntax highlighting should be enabled.
 |`hide_top_menu`               |not set if `false`|
-If true the top menu header and site header is hidden.
+If true the top menu header and site header are hidden.
 |`auto_hide_diff_table_header` |not set if `false`|
 If true the diff table header is automatically hidden when
 scrolling down more than half of a page.
@@ -1677,6 +1759,44 @@
 Number of spaces that should be used to display one tab.
 |===========================================
 
+[[edit-preferences-info]]
+=== EditPreferencesInfo
+The `EditPreferencesInfo` entity contains information about the edit
+preferences of a user.
+
+[options="header",cols="1,^1,5"]
+|===========================================
+|Field Name                    ||Description
+|`theme`                       ||
+The CodeMirror theme. Currently only a subset of light and dark
+CodeMirror themes are supported. Light themes `DEFAULT`, `ECLIPSE`,
+`ELEGANT`, `NEAT`. Dark themes `MIDNIGHT`, `NIGHT`, `TWILIGHT`.
+|`key_map_type`                ||
+The CodeMirror key map. Currently only a subset of key maps are
+supported: `DEFAULT`, `EMACS`, `VIM`.
+|`tab_size`                    ||
+Number of spaces that should be used to display one tab.
+|`line_length`                 ||
+Number of characters that should be displayed per line.
+|`cursor_blink_rate`           ||
+Half-period in milliseconds used for cursor blinking.
+Setting it to 0 disables cursor blinking.
+|`hide_top_menu`               |not set if `false`|
+If true the top menu header and site header is hidden.
+|`show_tabs`                   |not set if `false`|
+Whether tabs should be shown.
+|`show_whitespace_errors`      |not set if `false`|
+Whether whitespace errors should be shown.
+|`syntax_highlighting`         |not set if `false`|
+Whether syntax highlighting should be enabled.
+|`hide_line_numbers`           |not set if `false`|
+Whether line numbers should be hidden.
+|`match_brackets`              |not set if `false`|
+Whether matching brackets should be highlighted.
+|`auto_close_brackets`         |not set if `false`|
+Whether brackets and quotes should be auto-closed during typing.
+|===========================================
+
 [[email-info]]
 === EmailInfo
 The `EmailInfo` entity contains information about an email address of a
@@ -1729,6 +1849,15 @@
 link:https://tools.ietf.org/html/rfc4880#section-5.11[OpenPGP User IDs]
 associated with the public key.
 |`key`        |Not set for deleted keys|ASCII armored public key material.
+|`status`     |Not set for deleted keys|
+The result of server-side checks on the key; one of `BAD`, `OK`, or `TRUSTED`.
+`BAD` keys have serious problems and should not be used. If a key is `OK,
+inspecting only that key found no problems, but the system does not fully trust
+the key's origin. A `TRUSTED` key is valid, and the system knows enough about
+the key and its origin to trust it.
+|`problems`   |Not set for deleted keys|
+A list of human-readable problem strings found in the course of checking whether
+the key is valid and trusted.
 |========================
 
 [[gpg-keys-input]]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4b57827..7e96556 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -315,6 +315,14 @@
   link:#revision-info[RevisionInfo].
 --
 
+[[push-certificates]]
+--
+* `PUSH_CERTIFICATES`: include push certificate information in the
+  link:#revision-info[RevisionInfo]. Ignored if signed push is not
+  link:config-gerrit.html#receive.enableSignedPush[enabled] on the
+  server.
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -405,29 +413,36 @@
           },
           "files": {
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
-              "lines_deleted": 8
+              "lines_deleted": 8,
+              "size_delta": -412
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
-              "lines_inserted": 1
+              "lines_inserted": 1,
+              "size_delta": 23
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
               "lines_inserted": 11,
-              "lines_deleted": 19
+              "lines_deleted": 19,
+              "size_delta": -298
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
               "lines_inserted": 23,
-              "lines_deleted": 20
+              "lines_deleted": 20,
+              "size_delta": 132
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
               "status": "D",
-              "lines_deleted": 139
+              "lines_deleted": 139,
+              "size_delta": -5512
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
               "status": "A",
-              "lines_inserted": 204
+              "lines_inserted": 204,
+              "size_delta": 8345
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
-              "lines_deleted": 9
+              "lines_deleted": 9,
+              "size_delta": -343
             }
           }
         }
@@ -3254,11 +3269,13 @@
   {
     "/COMMIT_MSG": {
       "status": "A",
-      "lines_inserted": 7
+      "lines_inserted": 7,
+      "size_delta": 551
     },
     "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
       "lines_inserted": 5,
-      "lines_deleted": 3
+      "lines_deleted": 3,
+      "size_delta": 98
     }
   }
 ----
@@ -3347,10 +3364,13 @@
 The HTTP resource Content-Type is dependent on the file type: the
 applicable type for safe files, or "application/zip" for unsafe files.
 
-The `suffix` parameter can be specified to decorate the names of the files.
-The suffix is inserted between the base filename and the random component or
-extension, or appended to the filename if neither such component is present.
-Only the lowercase Roman letters a-z are permitted; other characters are ignored.
+The optional, integer-valued `parent` parameter can be specified to request
+the named file from a parent commit of the specified revision. The value is
+the 1-based index of the parent's position in the commit object. If the
+parameter is omitted or the value non-positive, the patch set is referenced.
+
+Filenames are decorated with a suffix of `_new` for the current patch,
+`_old` for the only parent, or `_oldN` for the Nth parent of many.
 
 .Request
 ----
@@ -4196,6 +4216,8 @@
 |`lines_deleted` |optional|
 Number of deleted lines. +
 Not set for binary files or if no lines were deleted.
+|`size_delta`    ||
+Number of bytes by which the file size increased/decreased.
 |=============================
 
 [[fix-input]]
@@ -4352,6 +4374,23 @@
 outcome of the fix.
 |===========================
 
+[[push-certificate-info]]
+=== PushCertificateInfo
+The `PushCertificateInfo` entity contains information about a push
+certificate provided when the user pushed for review with `git push
+--signed HEAD:refs/for/<branch>`. Only used when signed push is
+link:config-gerrit.html#receive.enableSignedPush[enabled] on the server.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name|Description
+|`certificate`|Signed certificate payload and GPG signature block.
+|`key`        |
+Information about the key that signed the push, along with any problems
+found while checking the signature or the key itself, as a
+link:rest-api-accounts.html#gpg-key-info[GpgKeyInfo] entity.
+|===========================
+
 [[rebase-input]]
 === RebaseInput
 The `RebaseInput` entity contains information for changing parent when rebasing.
@@ -4563,6 +4602,12 @@
 Gerrit-specific commit footers, as if this revision were submitted
 using the link:project-configuration.html#cherry_pick[Cherry Pick]
 submit type.
+|`push_certificate` |optional|
+If the link:#push-certificates[PUSH_CERTIFICATES] option is requested,
+contains the push certificate provided by the user when uploading this
+patch set as a link:#push-certificate-info[PushCertificateInfo] entity.
+This field is always set if the option is requested; if no push
+certificate was provided, it is set to an empty object.
 |===========================
 
 [[rule-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 7e117c5..b1b795c 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -60,7 +60,7 @@
       ]
     },
     "download": {
-      "schemes": [
+      "schemes": {
         "anonymous http": {
           "url": "http://gerrithost:8080/${project}",
           "commands": {
@@ -70,7 +70,7 @@
             "Cherry Pick": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
           },
           "clone_commands": {
-            "Clone": "git clone http://gerrithost:8080/${project}"
+            "Clone": "git clone http://gerrithost:8080/${project}",
             "Clone with commit-msg hook": "git clone http://gerrithost:8080/${project} \u0026\u0026 scp -p -P 29418 jdoe@gerrithost:hooks/commit-msg ${project}/.git/hooks/"
           }
         },
@@ -104,7 +104,7 @@
             "Clone with commit-msg hook": "git clone ssh://jdoe@gerrithost:29418/${project} \u0026\u0026 scp -p -P 29418 jdoe@gerrithost:hooks/commit-msg ${project}/.git/hooks/"
           }
         }
-      ],
+      },
       "archives": [
         "tgz",
         "tar",
@@ -1193,6 +1193,8 @@
 Custom base URL where Gerrit server documentation is located.
 (Documentation may still be available at /Documentation relative to the
 Gerrit base path even if this value is unset.)
+|`edit_gpg_keys`     |not set if `false`|
+Whether to enable the web UI for editing GPG keys.
 |`report_bug_url`    |optional|
 link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs].
 |`report_bug_text`   |optional, not set if default|
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index afdf36d..e0df4ca 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -172,6 +172,46 @@
   GET /groups/?n=25&S=50 HTTP/1.0
 ----
 
+[[suggest-group]]
+==== Suggest Group
+The `suggest` option indicates a user-entered string that
+should be auto-completed to group names.
+If this option is set and `n` is not set, then `n` defaults to 10.
+
+When using this option,
+the `project` or `p` option can be used to name the current project,
+to allow context-dependent suggestions.
+
+Not compatible with `visible-to-all`, `owned`, `user`, `match`, `q`,
+or `S`.
+(Attempts to use one of those options combined with `suggest` will
+error out.)
+
+.Request
+----
+  GET /groups/?suggest=ad&p=All-Projects HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "Administrators": {
+      "url": "#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b",
+      "options": {},
+      "description": "Gerrit Site Administrators",
+      "group_id": 1,
+      "owner": "Administrators",
+      "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b",
+      "id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b"
+    }
+  }
+----
+
 [[get-group]]
 === Get Group
 --
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 4658e2c..59184a8 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -732,6 +732,7 @@
     "use_signed_off_by": "INHERIT",
     "create_new_change_for_all_not_in_target": "INHERIT",
     "enable_signed_push": "INHERIT",
+    "require_signed_push": "INHERIT",
     "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
@@ -780,6 +781,11 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
+    "require_signed_push": {
+      "value": false,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "max_object_size_limit": {
       "value": "10m",
       "configured_value": "10m",
@@ -1453,6 +1459,80 @@
   ]
 ----
 
+[[tag-options]]
+==== Tag Options
+
+Limit(n)::
+Limit the number of tags to be included in the results.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?n=2 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v1.0",
+      "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Annotated tag",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 07:35:03.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    }
+  ]
+----
+
+Skip(s)::
+Skip the given number of tags from the beginning of the list.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?n=2&s=1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    },
+    {
+      "ref": "refs/tags/v3.0",
+      "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 09:02:16.000000000",
+        "tz": 540
+      }
+    }
+  ]
+----
+
+
 [[get-tag]]
 === Get Tag
 --
@@ -1908,9 +1988,14 @@
 valid link:user-changeid.html[Change-Id] footer in any commit uploaded
 for review is required. This does not apply to commits pushed directly
 to a branch or tag.
-|`enable_signed_push`                      |optional|
+|`enable_signed_push`|
+optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is enabled on the project.
+|`require_signed_push`|
+optional, not set if signed push is disabled
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+signed push validation is required on the project.
 |`max_object_size_limit`     ||
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index e262cb7..05932df 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -59,6 +59,8 @@
 To save edits, click the 'Save' button or press `CTRL-S`.  To return to the
 change screen, click the 'Close' button.
 
+Note that when editing the commit message, trailing blank lines will be stripped.
+
 image::images/inline-edit-full-screen-editor.png[width=800, link="images/inline-edit-full-screen-editor.png"]
 
 If there are unsaved edits when the 'Close' button is pressed, a dialog will
@@ -154,10 +156,8 @@
 [[not-implemented-features]]
 == Not Implemented Features
 
-* [PENDING CHANGE]
-The inline editor uses settings decided from the user's diff preferences, but those
-preferences are only modifiable from the side-by-side diff screen. It should be possible
-to open the preferences also from within the editor.
+* Support default configuration options for inline editor that an
+administrator has set in `refs/users/default:preferences.config` file.
 
 * Allow to rename files that are already contained in the change (from the file table).
 The same rename file dialog can be used with preselected and disabled original file
@@ -178,9 +178,6 @@
 ** "save-when-file-was-changed" or
 ** "close-when-no-changes"
 
-* Allow to activate different key maps, supported by CM: Emacs, Sublime, Vim. Load key
-maps dynamically. Currently default mode is used.
-
 * Implement conflict resolution during rebase of change edit using inline edit
 feature by creating new edit on top of current patch set with auto merge content
 
diff --git a/ReleaseNotes/ReleaseNotes-2.10.7.txt b/ReleaseNotes/ReleaseNotes-2.10.7.txt
new file mode 100644
index 0000000..28cf37b
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.7.txt
@@ -0,0 +1,19 @@
+Release notes for Gerrit 2.10.7
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.6.html[2.10.6].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.7.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.7.war]
+
+Bug Fixes
+---------
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3361[Issue 3361]:
+Synchronize Myers diff and Histogram diff invocations to prevent pack file
+corruption.
++
+See also the link:https://bugs.eclipse.org/bugs/show_bug.cgi?id=467467[
+bug report on JGit].
+
diff --git a/ReleaseNotes/ReleaseNotes-2.11.4.txt b/ReleaseNotes/ReleaseNotes-2.11.4.txt
new file mode 100644
index 0000000..6037edd
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.4.txt
@@ -0,0 +1,146 @@
+Release notes for Gerrit 2.11.4
+===============================
+
+Gerrit 2.11.4 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.4.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.4.war]
+
+Gerrit 2.11.4 includes the bug fixes done with
+link:ReleaseNotes-2.10.7.html[Gerrit 2.10.7]. These bug fixes are *not* listed
+in these release notes.
+
+There are no schema changes from link:ReleaseNotes-2.11.3.html[2.11.3].
+
+
+Bug Fixes
+---------
+
+* Fix NullPointerException in `ls-project` command with `--has-acl-for` option.
++
+Using the `--has-acl-for` option for external groups (e.g. LDAP groups) was
+causing a NullPointerException.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3328[Issue 3328]:
+Allow to push a tag that points to a non-commit object.
++
+When pushing a tag that points to a non-commit object, like
+link:https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tag/?id=v2.6.11[
+`v2.6.11` on linux-stable] which points to a tree, or
+link:https://git.eclipse.org/c/jgit/jgit.git/tag/?id=spearce-gpg-pub[
+`spearce-gpg-pub` on jgit] which points to a blob, Gerrit rejected the push with
+the error message 'missing object(s)'.
++
+Note: This was previously fixed in Gerrit version 2.11.1, but was inadvertently
+reverted in 2.11.2 and 2.11.3.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2817[Issue 2817]:
+Insert `Change-Id` footer into access right changes.
++
+When modifications of access rights were saved for review, the change
+did not have a `Change-Id` footer in the commit message.
+
+* Fix duplicated log lines after reloading a plugin.
++
+If a plugin was reloaded, logs emitted from the plugin were duplicated.
+
+* Remove `--recheck-mergeable` option from `reindex` command documentation.
++
+The `--recheck-mergeable` option was removed in Gerrit version 2.11.
+
+* Use the correct validation policy for commits created by Gerrit.
++
+Commits created by Gerrit were being validated in the same way as commits
+received from users.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3557[Issue 3557]:
+Disallow invalid reference patterns in project configuration.
++
+When editing a project configuration by using the UI or by submitting a change
+to `refs/meta/config`, it was possible to add a permission to an invalid
+reference pattern. This caused the project to be unavailable and the `ls-projects`
+command to fail whenever this project was encountered.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3574[Issue 3574]:
+Fix review labels with `AnyWithBlock` function.
++
+The review labels with `AnyWithBlock` with 0 and +1 values blocked submit when
+reviewers were added.
+
+* Fix ref in tag list for signed/annotated tags.
++
+The tag name from the header was used, rather than the ref name. In some cases
+this resulted in the wrong tag ref being listed.
+
+* Prevent user from bypassing `ref-update` hook through gerrit-created commits.
++
+If the user used the cherry-pick ability in the UI or via the REST API, they
+could put a commit on a branch that bypassed the requirements of the `ref-update`
+hook (such as that certain branches require QA-tickets to be referenced in the
+commit message).
+
+* Allow `InternalUsers` to see drafts.
++
+According to the documentation, `InternalUsers` should have full read access.
+This was not true, since `InternalUsers` could not see drafts.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2683[Issue 2683]:
+Fix non-ASCII password authentication failure under tomcat (LDAP).
++
+The authentication with LDAP failed when the password contained non-ASCII
+characters such as ä, ö, Ä, and Ö.
+
+* Do not double decode the login URL token.
++
+The login URL token used to redirect from the login servlet to the target page
+is already decoded and should not be decoded again.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3020[Issue 3020]:
+Include approvals specified on push in change message.
++
+When using the `%l` option to apply a review label on uploaded changes or
+patch sets, the applied label was not mentioned in the change message.
+
+* Fire the `comment-added` hook for approvals specified on push.
++
+When using the `%l` option to apply a review label on uploaded changes or
+patch sets, the `comment-added` hook was not being fired.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3602[Issue 3602]:
+Use uploader for approvals specified on push, not the committer.
++
+When using the `%l` option to apply a review label on uploaded changes or
+patch sets, the review label was in some cases applied as the committer rather
+than the uploader.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3531[Issue 3531]:
+Fix internal server error on unified diff screen for anonymous users.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2414[Issue 2414]:
+Improve detection of requiring sign-in.
++
+Some queries, such as the `has:*` operators, require the user to be signed in.
++
+Also, when handling a REST API failure, detect 'Invalid authentication' responses
+as also requiring a new session.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3052[Issue 3052]:
+Fix 'Conflicts With' list for merge commits.
++
+The 'Conflicts List' was not being populated correctly if the change being viewed
+was a merge commit, or if the change being viewed conflicted with an open merge
+commit.
+
+Plugin Bugfixes
+---------------
+
+* singleusergroup: Allow to add a user to a project's ACL using `user/username`.
++
+A user could not be added to a project's ACL unless the user already had READ
+permission in the project's ACL.
+
+* replication: Add waiting time and number of retries to replication log.
++
+Only the replication execution time was printed in the 'replication completed'
+log statement. The waiting time and retry count is added, to help debug
+replication delays.
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 262dc4f..e1831ad 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -4,6 +4,7 @@
 [[2_11]]
 Version 2.11.x
 --------------
+* link:ReleaseNotes-2.11.4.html[2.11.4]
 * link:ReleaseNotes-2.11.3.html[2.11.3]
 * link:ReleaseNotes-2.11.2.html[2.11.2]
 * link:ReleaseNotes-2.11.1.html[2.11.1]
@@ -12,6 +13,7 @@
 [[2_10]]
 Version 2.10.x
 --------------
+* link:ReleaseNotes-2.10.7.html[2.10.7]
 * link:ReleaseNotes-2.10.6.html[2.10.6]
 * link:ReleaseNotes-2.10.5.html[2.10.5]
 * link:ReleaseNotes-2.10.4.html[2.10.4]
diff --git a/bucklets/gerrit_plugin.bucklet b/bucklets/gerrit_plugin.bucklet
index ae7e1a2..cd2edae 100644
--- a/bucklets/gerrit_plugin.bucklet
+++ b/bucklets/gerrit_plugin.bucklet
@@ -14,7 +14,8 @@
 # When compiling from standalone cookbook-plugin, bucklets directory points
 # to cloned bucklets library that includes real gerrit_plugin.bucklet code.
 
-GERRIT_PLUGIN_API = ['//gerrit-plugin-api:lib']
 GERRIT_GWT_API = ['//gerrit-plugin-gwtui/gerrit:gwtui-api']
+GERRIT_PLUGIN_API = ['//gerrit-plugin-api:lib']
+GERRIT_TESTS = ['//gerrit-acceptance-framework:lib']
 
 STANDALONE_MODE = False
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
new file mode 100644
index 0000000..d8f0276
--- /dev/null
+++ b/gerrit-acceptance-framework/BUCK
@@ -0,0 +1,87 @@
+SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java'])
+
+DEPS = [
+  '//gerrit-gpg:gpg',
+  '//gerrit-pgm:daemon',
+  '//gerrit-pgm:util-nodep',
+  '//gerrit-server:testutil',
+  '//lib/auto:auto-value',
+  '//lib/httpcomponents:fluent-hc',
+  '//lib/httpcomponents:httpclient',
+  '//lib/httpcomponents:httpcore',
+  '//lib/jgit:junit',
+  '//lib/log:impl_log4j',
+  '//lib/log:log4j',
+]
+
+PROVIDED = [
+  '//gerrit-common:annotations',
+  '//gerrit-common:server',
+  '//gerrit-extension-api:api',
+  '//gerrit-httpd:httpd',
+  '//gerrit-lucene:lucene',
+  '//gerrit-pgm:init',
+  '//gerrit-reviewdb:server',
+  '//gerrit-server:server',
+  '//lib:gson',
+  '//lib/jgit:jgit',
+  '//lib:jsch',
+  '//lib/mina:sshd',
+  '//lib:servlet-api-3_1',
+]
+
+java_binary(
+  name = 'acceptance-framework',
+  deps = [':lib'],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'lib',
+  srcs = SRCS,
+  exported_deps = DEPS + [
+    '//lib:truth',
+  ],
+  provided_deps = PROVIDED + [
+    '//lib:gwtorm',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'src',
+  srcs = SRCS,
+  visibility = ['PUBLIC'],
+)
+
+# The above java_sources produces a .jar somewhere in the depths of
+# buck-out, but it does not bring it to
+# buck-out/gen/gerrit-acceptance-framework/gerrit-acceptance-framework-src.jar.
+# We fix that by the following java_binary.
+java_binary(
+  name = 'acceptance-framework-src',
+  deps = [ ':src' ],
+  visibility = ['PUBLIC'],
+)
+
+java_doc(
+  name = 'acceptance-framework-javadoc',
+  title = 'Gerrit Acceptance Test Framework Documentation',
+  pkgs = [' com.google.gerrit.acceptance'],
+  paths = ['src/test/java'],
+  srcs = SRCS,
+  deps = DEPS + PROVIDED + [
+    '//lib:guava',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice_library',
+    '//lib/guice:guice-servlet',
+    '//lib/guice:javax-inject',
+    '//lib:gwtorm_client',
+    '//lib:junit__jar',
+    '//lib:truth__jar',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
new file mode 100644
index 0000000..ca1ecd9
--- /dev/null
+++ b/gerrit-acceptance-framework/pom.xml
@@ -0,0 +1,59 @@
+<project>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.google.gerrit</groupId>
+  <artifactId>gerrit-acceptance-framework</artifactId>
+  <version>2.12-SNAPSHOT</version>
+  <packaging>jar</packaging>
+  <name>Gerrit Code Review - Acceptance Test Framework</name>
+  <description>API for Gerrit Plugins</description>
+  <url>https://www.gerritcodereview.com/</url>
+
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <scm>
+    <url>https://gerrit.googlesource.com/gerrit</url>
+    <connection>https://gerrit.googlesource.com/gerrit</connection>
+  </scm>
+
+  <developers>
+    <developer>
+      <name>Dave Borowitz</name>
+    </developer>
+    <developer>
+      <name>David Pursehouse</name>
+    </developer>
+    <developer>
+      <name>Edwin Kempin</name>
+    </developer>
+    <developer>
+      <name>Martin Fick</name>
+    </developer>
+    <developer>
+      <name>Saša Živkov</name>
+    </developer>
+    <developer>
+      <name>Shawn Pearce</name>
+    </developer>
+  </developers>
+
+  <mailingLists>
+    <mailingList>
+      <name>Repo and Gerrit Discussion</name>
+      <post>repo-discuss@googlegroups.com</post>
+      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
+      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
+      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
+    </mailingList>
+  </mailingLists>
+
+  <issueManagement>
+    <url>http://code.google.com/p/gerrit/issues/list</url>
+    <system>Google Code Issue Tracker</system>
+  </issueManagement>
+</project>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
similarity index 94%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 8d97136..7dbbfb5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.block;
 
+import com.google.common.base.Function;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
@@ -52,6 +55,7 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -170,6 +174,9 @@
   protected ReviewDb db;
   protected Project.NameKey project;
 
+  @Inject
+  protected NotesMigration notesMigration;
+
   @Rule
   public ExpectedException exception = ExpectedException.none();
 
@@ -226,12 +233,22 @@
     return cfg.getBoolean("change", null, "submitWholeTopic", false);
   }
 
+  private static boolean isNoteDbTestEnabled() {
+    final String[] RUN_FLAGS = {"yes", "y", "true"};
+    String value = System.getenv("GERRIT_ENABLE_NOTEDB");
+    return value != null &&
+        Arrays.asList(RUN_FLAGS).contains(value.toLowerCase());
+  }
+
   protected void beforeTest(Description description) throws Exception {
     GerritServer.Description classDesc =
       GerritServer.Description.forTestClass(description, configName);
     GerritServer.Description methodDesc =
       GerritServer.Description.forTestMethod(description, configName);
 
+    if (isNoteDbTestEnabled()) {
+      NotesMigration.setAllEnabledConfig(baseConfig);
+    }
     baseConfig.setString("gerrit", null, "tempSiteDir",
         tempSiteDir.getRoot().getPath());
     if (classDesc.equals(methodDesc)) {
@@ -594,4 +611,17 @@
       .revision(1)
       .actions();
   }
+
+  protected void assertSubmittedTogether(String chId, String... expected)
+      throws Exception {
+    List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
+    assertThat(actual).hasSize(expected.length);
+    assertThat(Iterables.transform(actual,
+        new Function<ChangeInfo, String>() {
+      @Override
+      public String apply(ChangeInfo input) {
+        return input.changeId;
+      }
+    })).containsExactly((Object[])expected).inOrder();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
similarity index 97%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 43ad799..34379a1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -76,7 +76,7 @@
     }
 
     @Override
-    public CurrentUser getCurrentUser() {
+    public CurrentUser getUser() {
       if (user == null) {
         throw new IllegalStateException("user == null, forgot to set it?");
       }
@@ -153,7 +153,7 @@
   }
 
   private Context newContinuingContext(Context ctx) {
-    return new Context(ctx, ctx.getSession(), ctx.getCurrentUser());
+    return new Context(ctx, ctx.getSession(), ctx.getUser());
   }
 
   public Context set(Context ctx) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GcAssert.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GcAssert.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
similarity index 98%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
index c02b9e5..c16eed7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -187,7 +187,7 @@
     }
 
     @Override
-    public CurrentUser getCurrentUser() {
+    public CurrentUser getUser() {
       return get(USER_KEY, null);
     }
 
@@ -326,8 +326,7 @@
           throw new ServiceNotAuthorizedException();
         }
 
-        IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
-        rp.setRefLogIdent(user.newRefLogIdent());
+        rp.setRefLogIdent(ctl.getUser().asIdentifiedUser().newRefLogIdent());
         rp.setTimeout(config.getTimeout());
         rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/NoHttpd.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
similarity index 96%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
index 4913488..f0b9f46 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.server.config.SitePaths;
@@ -27,7 +29,6 @@
 import java.io.InputStream;
 import java.lang.ProcessBuilder.Redirect;
 import java.net.URL;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -134,7 +135,7 @@
     Path buckFile = pluginSource.resolve("BUCK");
     byte[] bytes = Files.readAllBytes(buckFile);
     String buckContent =
-        new String(bytes, StandardCharsets.UTF_8).replaceAll("\\s+", "");
+        new String(bytes, UTF_8).replaceAll("\\s+", "");
     Matcher matcher =
         Pattern.compile("gerrit_plugin\\(name='(.*?)'").matcher(buckContent);
     if (matcher.find()) {
@@ -189,7 +190,7 @@
 
   private Properties loadBuckProperties() throws IOException {
     Properties properties = new Properties();
-    Path propertiesPath = gen.resolve("tools").resolve("buck.properties");
+    Path propertiesPath = gen.resolve(Paths.get("tools/buck/buck.properties"));
     if (Files.exists(propertiesPath)) {
       try (InputStream in = Files.newInputStream(propertiesPath)) {
         properties.load(in);
@@ -202,6 +203,7 @@
     SitePaths sitePath = new SitePaths(testSite);
     pluginsSitePath = Files.createDirectories(sitePath.plugins_dir);
     Files.createDirectories(sitePath.tmp_dir);
+    Files.createDirectories(sitePath.etc_dir);
   }
 
   private void copyJarToTestSite() throws IOException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
similarity index 87%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
index 6c7dbfe..261b894 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
-import java.nio.charset.StandardCharsets;
 
 public class RestResponse extends HttpResponse {
 
@@ -30,9 +30,8 @@
   @Override
   public Reader getReader() throws IllegalStateException, IOException {
     if (reader == null && response.getEntity() != null) {
-      reader =
-          new InputStreamReader(response.getEntity().getContent(),
-              StandardCharsets.UTF_8);
+      reader = new InputStreamReader(
+          response.getEntity().getContent(), UTF_8);
       reader.skip(JSON_MAGIC.length);
     }
     return reader;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
similarity index 94%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 98459b3..aee629d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.common.base.Charsets;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Preconditions;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.restapi.RawInput;
@@ -30,7 +31,6 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
 
 public class RestSession extends HttpSession {
 
@@ -79,7 +79,7 @@
       put.addHeader(new BasicHeader("Content-Type", "application/json"));
       put.body(new StringEntity(
           OutputFormat.JSON_COMPACT.newGson().toJson(content),
-          Charsets.UTF_8.name()));
+          UTF_8));
     }
     return execute(put);
   }
@@ -105,7 +105,7 @@
       post.addHeader(new BasicHeader("Content-Type", "application/json"));
       post.body(new StringEntity(
           OutputFormat.JSON_COMPACT.newGson().toJson(content),
-          Charsets.UTF_8.name()));
+          UTF_8));
     }
     return execute(post);
   }
@@ -116,7 +116,7 @@
 
 
   public static RawInput newRawInput(String content) {
-    return newRawInput(content.getBytes(StandardCharsets.UTF_8));
+    return newRawInput(content.getBytes(UTF_8));
   }
 
   public static RawInput newRawInput(final byte[] bytes) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index 3adab73..0a39ea7 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -2,10 +2,10 @@
   name = 'lib',
   srcs = glob(['src/test/java/com/google/gerrit/acceptance/*.java']),
   exported_deps = [
+    '//gerrit-acceptance-framework:lib',
     '//gerrit-common:annotations',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
-    '//gerrit-gpg:gpg',
     '//gerrit-gpg:testutil',
     '//gerrit-launcher:launcher',
     '//gerrit-lucene:lucene',
@@ -15,8 +15,8 @@
     '//gerrit-pgm:util',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
-    '//gerrit-server/src/main/prolog:common',
     '//gerrit-server:testutil',
+    '//gerrit-server/src/main/prolog:common',
     '//gerrit-sshd:sshd',
 
     '//lib:args4j',
@@ -26,21 +26,13 @@
     '//lib:h2',
     '//lib:jsch',
     '//lib:servlet-api-3_1',
-    '//lib:truth',
 
-    '//lib/auto:auto-value',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcprov',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
-    '//lib/httpcomponents:fluent-hc',
-    '//lib/httpcomponents:httpclient',
-    '//lib/httpcomponents:httpcore',
     '//lib/jgit:jgit',
-    '//lib/jgit:junit',
-    '//lib/log:impl_log4j',
-    '//lib/log:log4j',
     '//lib/mina:sshd',
   ],
   visibility = [
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 5baaa18..39296d0 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
@@ -19,6 +19,10 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.allValidKeys;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Function;
@@ -40,7 +44,6 @@
 import com.google.gerrit.gpg.testutil.TestKey;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.inject.Inject;
@@ -204,7 +207,7 @@
 
   @Test
   public void addGpgKey() throws Exception {
-    TestKey key = TestKey.key1();
+    TestKey key = validKeyWithoutExpiration();
     String id = key.getKeyIdString();
     addExternalIdEmail(admin, "test1@example.com");
 
@@ -220,7 +223,7 @@
   @Test
   public void reAddExistingGpgKey() throws Exception {
     addExternalIdEmail(admin, "test5@example.com");
-    TestKey key = TestKey.key5();
+    TestKey key = validKeyWithSecondUserId();
     String id = key.getKeyIdString();
     PGPPublicKey pk = key.getPublicKey();
 
@@ -243,7 +246,7 @@
 
     db.accountExternalIds().insert(Collections.singleton(extId));
 
-    TestKey key = TestKey.key5();
+    TestKey key = validKeyWithSecondUserId();
     addGpgKey(key.getPublicKeyArmored());
     setApiUser(user);
 
@@ -254,7 +257,7 @@
 
   @Test
   public void listGpgKeys() throws Exception {
-    List<TestKey> keys = TestKey.allValidKeys();
+    List<TestKey> keys = allValidKeys();
     List<String> toAdd = new ArrayList<>(keys.size());
     for (TestKey key : keys) {
       addExternalIdEmail(admin,
@@ -267,7 +270,7 @@
 
   @Test
   public void deleteGpgKey() throws Exception {
-    TestKey key = TestKey.key1();
+    TestKey key = validKeyWithoutExpiration();
     String id = key.getKeyIdString();
     addExternalIdEmail(admin, "test1@example.com");
     addGpgKey(key.getPublicKeyArmored());
@@ -283,13 +286,13 @@
 
   @Test
   public void addAndRemoveGpgKeys() throws Exception {
-    for (TestKey key : TestKey.allValidKeys()) {
+    for (TestKey key : allValidKeys()) {
       addExternalIdEmail(admin,
           PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
     }
-    TestKey key1 = TestKey.key1();
-    TestKey key2 = TestKey.key2();
-    TestKey key5 = TestKey.key5();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
+    TestKey key5 = validKeyWithSecondUserId();
 
     Map<String, GpgKeyInfo> infos = gApi.accounts().self().putGpgKeys(
         ImmutableList.of(
@@ -374,8 +377,7 @@
     }
 
     // Check raw external IDs.
-    Account.Id currAccountId =
-        ((IdentifiedUser) atrScope.get().getCurrentUser()).getAccountId();
+    Account.Id currAccountId = atrScope.get().getUser().getAccountId();
     assertThat(
         GpgKeys.getGpgExtIds(db, currAccountId)
           .transform(new Function<AccountExternalId, String>() {
@@ -411,6 +413,8 @@
     assertThat(actual.userIds).named(id).containsExactlyElementsIn(userIds);
     assertThat(actual.key).named(id)
         .startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
+    assertThat(actual.status).isEqualTo(GpgKeyInfo.Status.TRUSTED);
+    assertThat(actual.problems).isEmpty();
   }
 
   private void addExternalIdEmail(TestAccount account, String email)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
index 1152d88..814dcf4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-account',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
index 1152d88..5db2054 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-change',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 967575a..3d24c71 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
@@ -85,20 +85,32 @@
   @Test
   public void abandon() throws Exception {
     PushOneCommit.Result r = createChange();
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes()
         .id(r.getChangeId())
         .abandon();
+    ChangeInfo info = get(r.getChangeId());
+    assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("abandoned");
   }
 
   @Test
   public void restore() throws Exception {
     PushOneCommit.Result r = createChange();
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes()
         .id(r.getChangeId())
         .abandon();
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.ABANDONED);
+
     gApi.changes()
         .id(r.getChangeId())
         .restore();
+    ChangeInfo info = get(r.getChangeId());
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("restored");
   }
 
   @Test
@@ -499,4 +511,28 @@
     assertThat(approval._accountId).isEqualTo(user.id.get());
     assertThat(approval.value).isNull();
   }
+
+  @Test
+  public void pushCertificates() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = amendChange(r1.getChangeId());
+
+    ChangeInfo info = gApi.changes()
+        .id(r1.getChangeId())
+        .get(EnumSet.of(
+            ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.PUSH_CERTIFICATES));
+
+    RevisionInfo rev1 = info.revisions.get(r1.getCommit().name());
+    assertThat(rev1).isNotNull();
+    assertThat(rev1.pushCertificate).isNotNull();
+    assertThat(rev1.pushCertificate.certificate).isNull();
+    assertThat(rev1.pushCertificate.key).isNull();
+
+    RevisionInfo rev2 = info.revisions.get(r2.getCommit().name());
+    assertThat(rev2).isNotNull();
+    assertThat(rev2.pushCertificate).isNotNull();
+    assertThat(rev2.pushCertificate.certificate).isNull();
+    assertThat(rev2.pushCertificate.key).isNull();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
index 1152d88..4918a95 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-config',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
index 332459a..06be8ee 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
@@ -1,12 +1,13 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-group',
   srcs = glob(['*IT.java']),
   deps = [
     ':util',
     '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
   ],
-  labels = ['rest']
+  labels = ['api']
 )
 
 java_library(
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
index 5b8b87f..c72edd7 100644
--- 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
@@ -402,6 +402,13 @@
   }
 
   @Test
+  public void testSuggestGroup() throws Exception {
+    Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap();
+    assertThat(groups).containsKey("Administrators");
+    assertThat(groups).hasSize(1);
+  }
+
+  @Test
   public void testAllGroupInfoFieldsSetCorrectly() throws Exception {
     AccountGroup adminGroup = getFromCache("Administrators");
     Map<String, GroupInfo> groups =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
index 1152d88..9dab9f8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-project',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
index 1152d88..c916755 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-revision',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 5429b95..3e970e4 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
@@ -18,6 +18,7 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
 import com.google.common.base.Predicate;
@@ -53,13 +54,13 @@
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
-import java.nio.charset.StandardCharsets;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 @NoHttpd
@@ -428,7 +429,7 @@
         .content();
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
-    String res = new String(os.toByteArray(), StandardCharsets.UTF_8);
+    String res = new String(os.toByteArray(), UTF_8);
     assertThat(res).isEqualTo(FILE_CONTENT);
   }
 
@@ -554,11 +555,13 @@
         .patch();
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
-    String res = new String(os.toByteArray(), StandardCharsets.UTF_8);
-    DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
+    String res = new String(os.toByteArray(), UTF_8);
     ChangeInfo change = changeApi.get();
     RevisionInfo rev = change.revisions.get(change.currentRevision);
-    String date = dateFormat.format(rev.commit.author.date);
+    DateFormat df = new SimpleDateFormat(
+        "EEE, dd MMM yyyy HH:mm:ss Z",
+        Locale.US);
+    String date = df.format(rev.commit.author.date);
     assertThat(res).isEqualTo(
         String.format(PATCH, r.getCommitId().name(), date, r.getChangeId()));
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
index be6fcdc..c3274db 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
@@ -1,10 +1,11 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'edit',
   srcs = ['ChangeEditIT.java'],
-  labels = ['edit'],
   deps = [
     '//lib/commons:codec',
     '//lib/joda:joda-time',
   ],
+  labels = ['edit'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 50bafbe..ed20e24 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
@@ -77,7 +77,6 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.Iterator;
@@ -319,7 +318,7 @@
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getParentCount()).isEqualTo(0);
 
-    String msg = String.format("New commit message\n\nChange-Id: %s",
+    String msg = String.format("New commit message\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(modifier.modifyMessage(edit.get(), msg))
         .isEqualTo(RefUpdate.Result.FORCED);
@@ -346,8 +345,9 @@
     assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
         .isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-
-    String msg = String.format("New commit message\n\nChange-Id: %s",
+    assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage());
+    assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage() + "\n\n");
+    String msg = String.format("New commit message\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(modifier.modifyMessage(edit.get(), msg)).isEqualTo(
         RefUpdate.Result.FORCED);
@@ -374,7 +374,7 @@
         .isEqualTo(SC_NOT_FOUND);
     EditMessage.Input in = new EditMessage.Input();
     in.message = String.format("New commit message\n\n" +
-        CONTENT_NEW2_STR + "\n\nChange-Id: %s",
+        CONTENT_NEW2_STR + "\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
         .isEqualTo(SC_NO_CONTENT);
@@ -384,7 +384,7 @@
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage())
         .isEqualTo(in.message);
-    in.message = String.format("New commit message2\n\nChange-Id: %s",
+    in.message = String.format("New commit message2\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
         .isEqualTo(SC_NO_CONTENT);
@@ -713,6 +713,14 @@
     assertThat(approvals.get(0).value).isEqualTo(1);
   }
 
+  private void assertUnchangedMessage(Optional<ChangeEdit> edit, String message)
+      throws Exception {
+    exception.expect(UnchangedCommitMessageException.class);
+    exception.expectMessage(
+        "New commit message cannot be same as existing commit message");
+    modifier.modifyMessage(edit.get(), message);
+  }
+
   @Test
   public void testHasEditPredicate() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
@@ -749,21 +757,21 @@
   private String newChange(PersonIdent ident) throws Exception {
     PushOneCommit push =
         pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME,
-            new String(CONTENT_OLD, StandardCharsets.UTF_8));
+            new String(CONTENT_OLD, UTF_8));
     return push.to("refs/for/master").getChangeId();
   }
 
   private String amendChange(PersonIdent ident, String changeId) throws Exception {
     PushOneCommit push =
         pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME2,
-            new String(CONTENT_NEW2, StandardCharsets.UTF_8), changeId);
+            new String(CONTENT_NEW2, UTF_8), changeId);
     return push.to("refs/for/master").getChangeId();
   }
 
   private String newChange2(PersonIdent ident) throws Exception {
     PushOneCommit push =
         pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME,
-            new String(CONTENT_OLD, StandardCharsets.UTF_8));
+            new String(CONTENT_OLD, UTF_8));
     return push.rm("refs/for/master").getChangeId();
   }
 
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 080b767..900d85a 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,8 +16,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -27,25 +31,20 @@
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testutil.ConfigSuite;
-import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeUtils;
+import org.joda.time.DateTimeUtils.MillisProvider;
+import org.junit.AfterClass;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
-
-  @Inject
-  private NotesMigration notesMigration;
-
   protected enum Protocol {
     // TODO(dborowitz): TEST.
     SSH, HTTP
@@ -53,6 +52,24 @@
 
   private String sshUrl;
 
+  @BeforeClass
+  public static void setTimeForTesting() {
+    final long clockStepMs = MILLISECONDS.convert(1, SECONDS);
+    final AtomicLong clockMs = new AtomicLong(
+        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
+    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
+      @Override
+      public long getMillis() {
+        return clockMs.getAndAdd(clockStepMs);
+      }
+    });
+  }
+
+  @AfterClass
+  public static void restoreTime() {
+    DateTimeUtils.setCurrentMillisSystem();
+  }
+
   @Before
   public void setUp() throws Exception {
     sshUrl = sshSession.getUrl();
@@ -177,6 +194,8 @@
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).name).isEqualTo("Administrator");
     assertThat(cr.all.get(0).value).isEqualTo(1);
+    assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
+        "Uploaded patch set 1: Code-Review+1.");
 
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
@@ -185,9 +204,64 @@
 
     ci = get(r.getChangeId());
     cr = ci.labels.get("Code-Review");
+    assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
+        "Uploaded patch set 2: Code-Review+2.");
+
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).name).isEqualTo("Administrator");
     assertThat(cr.all.get(0).value).isEqualTo(2);
+
+    push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "c.txt", "moreContent", r.getChangeId());
+    r = push.to("refs/for/master/%l=Code-Review+2");
+    ci = get(r.getChangeId());
+    assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
+        "Uploaded patch set 3.");
+  }
+
+  /**
+   * There was a bug that allowed a user with Forge Committer Identity access
+   * right to upload a commit and put *votes on behalf of another user* on it.
+   * This test checks that this is not possible, but that the votes that are
+   * specified on push are applied only on behalf of the uploader.
+   *
+   * This particular bug only occurred when there was more than one label
+   * defined. However to test that the votes that are specified on push are
+   * applied on behalf of the uploader a single label is sufficient.
+   */
+  @Test
+  public void testPushForMasterWithApprovalsForgeCommitterButNoForgeVote()
+      throws Exception {
+    // Create a commit with "User" as author and committer
+    RevCommit c = commitBuilder()
+        .author(user.getIdent())
+        .committer(user.getIdent())
+        .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+        .message(PushOneCommit.SUBJECT)
+        .create();
+
+    // Push this commit as "Administrator" (requires Forge Committer Identity)
+    pushHead(testRepo, "refs/for/master/%l=Code-Review+1", false);
+
+    // Expected Code-Review votes:
+    // 1. 0 from User (committer):
+    //    When the committer is forged, the committer is automatically added as
+    //    reviewer, hence we expect a dummy 0 vote for the committer.
+    // 2. +1 from Administrator (uploader):
+    //    On push Code-Review+1 was specified, hence we expect a +1 vote from
+    //    the uploader.
+    ChangeInfo ci = get(GitUtil.getChangeId(testRepo, c).get());
+    LabelInfo cr = ci.labels.get("Code-Review");
+    assertThat(cr.all).hasSize(2);
+    int indexAdmin = admin.fullName.equals(cr.all.get(0).name) ? 0 : 1;
+    int indexUser = indexAdmin == 0 ? 1 : 0;
+    assertThat(cr.all.get(indexAdmin).name).isEqualTo(admin.fullName);
+    assertThat(cr.all.get(indexAdmin).value.intValue()).isEqualTo(1);
+    assertThat(cr.all.get(indexUser).name).isEqualTo(user.fullName);
+    assertThat(cr.all.get(indexUser).value.intValue()).isEqualTo(0);
+    assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
+        "Uploaded patch set 1: Code-Review+1.");
   }
 
   @Test
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
index 2dbbb16..53412cb 100644
--- 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
@@ -62,14 +62,14 @@
     return pushChangeTo(repo, "refs/heads/" + branch, "some change", "");
   }
 
-  protected void createSubscription(TestRepository<?> repo, String branch,
+  protected void createSubmoduleSubscription(TestRepository<?> repo, String branch,
       String subscribeToRepo, String subscribeToBranch) throws Exception {
     Config config = new Config();
-    prepareSubscriptionConfigEntry(config, subscribeToRepo, subscribeToBranch);
-    pushSubscriptionConfig(repo, branch, config);
+    prepareSubmoduleConfigEntry(config, subscribeToRepo, subscribeToBranch);
+    pushSubmoduleConfig(repo, branch, config);
   }
 
-  protected void prepareSubscriptionConfigEntry(Config config,
+  protected void prepareSubmoduleConfigEntry(Config config,
       String subscribeToRepo, String subscribeToBranch) {
     subscribeToRepo = name(subscribeToRepo);
     // The submodule subscription module checks for gerrit.canonicalWebUrl to
@@ -79,10 +79,12 @@
         + subscribeToRepo;
     config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
     config.setString("submodule", subscribeToRepo, "url", url);
-    config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+    if (subscribeToBranch != null) {
+      config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+    }
   }
 
-  protected void pushSubscriptionConfig(TestRepository<?> repo,
+  protected void pushSubmoduleConfig(TestRepository<?> repo,
       String branch, Config config) throws Exception {
 
     repo.branch("HEAD").commit().insertChangeId()
@@ -101,13 +103,14 @@
     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());
+    try (RevWalk rw = repo.getRevWalk()) {
+      RevCommit c = rw.parseCommit(commitId);
+      rw.parseBody(c.getTree());
 
-    RevTree tree = c.getTree();
-    RevObject actualId = repo.get(tree, submodule);
+      RevTree tree = c.getTree();
+      RevObject actualId = repo.get(tree, submodule);
 
-    assertThat(actualId).isEqualTo(expectedId);
+      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 446a183..f6796a5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
@@ -1,28 +1,22 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  srcs = [
-    'DraftChangeBlockedIT.java',
-    'ForcePushIT.java',
-    'SubmitOnPushIT.java',
-    'SubmoduleSubscriptionsWholeTopicMergeIT.java',
-    'SubmoduleSubscriptionsIT.java',
-    'VisibleRefFilterIT.java',
+  group = 'git',
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':submodule_util',
+    ':push_for_review',
   ],
-  deps = [':submodule_util'],
-  labels = ['git'],
-)
-
-acceptance_tests(
-  srcs = ['HttpPushForReviewIT.java', 'SshPushForReviewIT.java'],
-  deps = [':push_for_review'],
   labels = ['git'],
 )
 
 java_library(
   name = 'push_for_review',
   srcs = ['AbstractPushForReview.java'],
-  deps = ['//gerrit-acceptance-tests:lib'],
+  deps = [
+    '//gerrit-acceptance-tests:lib',
+    '//lib/joda:joda-time',
+  ],
 )
 
 java_library(
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 9a8bb51..78ffa20 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
@@ -80,8 +80,6 @@
   @Test
   public void submitOnPushWithAnnotatedTag() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
-    grant(Permission.CREATE, project, "refs/tags/*");
-    grant(Permission.PUSH, project, "refs/tags/*");
     PushOneCommit.AnnotatedTag tag =
         new PushOneCommit.AnnotatedTag("v1.0", "annotation", admin.getIdent());
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
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
index 707852f..0efeb94 100644
--- 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
@@ -33,7 +33,7 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
@@ -45,7 +45,7 @@
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
@@ -58,7 +58,7 @@
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
 
     // The first update doesn't include any commit messages
     ObjectId subRepoId = pushChangeTo(subRepo, "master");
@@ -82,7 +82,7 @@
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
     // The first update doesn't include the rev log
@@ -110,7 +110,7 @@
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
@@ -133,7 +133,7 @@
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
@@ -155,7 +155,7 @@
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    createSubscription(superRepo, "master", "subscribed-to-project", "foo");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "foo");
     ObjectId subFoo = pushChangeTo(subRepo, "foo");
     pushChangeTo(subRepo, "master");
 
@@ -169,8 +169,8 @@
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubscription(subRepo, "master", "super-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(subRepo, "master", "super-project", "master");
 
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
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
index 086c205..e4a054a 100644
--- 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
@@ -41,7 +41,7 @@
   public void testSubscriptionUpdateOfManyChanges() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
 
     ObjectId subHEAD = subRepo.branch("HEAD").commit().insertChangeId()
         .message("some change")
@@ -97,10 +97,10 @@
     TestRepository<?> sub3 = createProjectWithPush("sub3");
 
     Config config = new Config();
-    prepareSubscriptionConfigEntry(config, "sub1", "master");
-    prepareSubscriptionConfigEntry(config, "sub2", "master");
-    prepareSubscriptionConfigEntry(config, "sub3", "master");
-    pushSubscriptionConfig(superRepo, "master", config);
+    prepareSubmoduleConfigEntry(config, "sub1", "master");
+    prepareSubmoduleConfigEntry(config, "sub2", "master");
+    prepareSubmoduleConfigEntry(config, "sub3", "master");
+    pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
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 21efc87..a9a7dfa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -34,35 +34,20 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.Util;
-import com.google.gerrit.testutil.ConfigSuite;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.List;
 
-@RunWith(ConfigSuite.class)
 @NoHttpd
 public class VisibleRefFilterIT extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbWriteEnabled() {
-    Config cfg = new Config();
-    cfg.setBoolean("notedb", "changes", "write", true);
-    return cfg;
-  }
-
-  @Inject
-  private NotesMigration notesMigration;
-
   @Inject
   private ChangeEditModifier editModifier;
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
index 00b53f9..ff167ac 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
@@ -1,7 +1,8 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'pgm',
   srcs = glob(['*IT.java']),
-  labels = ['pgm'],
   source_under_test = ['//gerrit-pgm:pgm'],
+  labels = ['pgm'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
index c538b85..5d0e7df 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.pgm;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.Files;
 import com.google.gerrit.launcher.GerritLauncher;
@@ -26,7 +27,6 @@
 import org.junit.Test;
 
 import java.io.File;
-import java.nio.charset.StandardCharsets;
 
 public class RebuildNotedbIT {
   private File sitePath;
@@ -48,7 +48,7 @@
     initSite();
     Files.append(NotesMigration.allEnabledConfig().toText(),
         new File(sitePath.toString(), "etc/gerrit.config"),
-        StandardCharsets.UTF_8);
+        UTF_8);
     runGerrit("RebuildNotedb", "-d", sitePath.toString(),
         "--show-stack-trace");
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
index f081ada..b7c1819 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-account',
   srcs = glob(['*IT.java']),
   deps = [':util'],
   labels = ['rest']
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
new file mode 100644
index 0000000..8770c3c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class EditPreferencesIT extends AbstractDaemonTest {
+  @Test
+  public void getSetEditPreferences() throws Exception {
+    String endPoint = "/accounts/" + admin.email + "/preferences.edit";
+    RestResponse r = adminSession.get(endPoint);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    EditPreferencesInfo out = getEditPrefInfo(r);
+
+    assertThat(out.lineLength).isEqualTo(100);
+    assertThat(out.tabSize).isEqualTo(8);
+    assertThat(out.cursorBlinkRate).isEqualTo(0);
+    assertThat(out.hideTopMenu).isNull();
+    assertThat(out.showTabs).isTrue();
+    assertThat(out.showWhitespaceErrors).isNull();
+    assertThat(out.syntaxHighlighting).isTrue();
+    assertThat(out.hideLineNumbers).isNull();
+    assertThat(out.matchBrackets).isTrue();
+    assertThat(out.autoCloseBrackets).isNull();
+    assertThat(out.theme).isEqualTo(Theme.DEFAULT);
+    assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
+
+    // change some default values
+    out.lineLength = 80;
+    out.tabSize = 4;
+    out.cursorBlinkRate = 500;
+    out.hideTopMenu = true;
+    out.showTabs = false;
+    out.showWhitespaceErrors = true;
+    out.syntaxHighlighting = false;
+    out.hideLineNumbers = true;
+    out.matchBrackets = false;
+    out.autoCloseBrackets = true;
+    out.theme = Theme.TWILIGHT;
+    out.keyMapType = KeyMapType.EMACS;
+
+    r = adminSession.put(endPoint, out);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+
+    r = adminSession.get(endPoint);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    EditPreferencesInfo info = getEditPrefInfo(r);
+    assertEditPreferences(info, out);
+  }
+
+  private EditPreferencesInfo getEditPrefInfo(RestResponse r)
+      throws IOException {
+    return newGson().fromJson(r.getReader(),
+        EditPreferencesInfo.class);
+  }
+
+  private void assertEditPreferences(EditPreferencesInfo out,
+      EditPreferencesInfo in) {
+    assertThat(out.lineLength).isEqualTo(in.lineLength);
+    assertThat(out.tabSize).isEqualTo(in.tabSize);
+    assertThat(out.cursorBlinkRate).isEqualTo(in.cursorBlinkRate);
+    assertThat(out.hideTopMenu).isEqualTo(in.hideTopMenu);
+    assertThat(out.showTabs).isNull();
+    assertThat(out.showWhitespaceErrors).isEqualTo(in.showWhitespaceErrors);
+    assertThat(out.syntaxHighlighting).isNull();
+    assertThat(out.hideLineNumbers).isEqualTo(in.hideLineNumbers);
+    assertThat(out.matchBrackets).isNull();
+    assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
+    assertThat(out.theme).isEqualTo(in.theme);
+    assertThat(out.keyMapType).isEqualTo(in.keyMapType);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 438ebec..ef3faa5 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
@@ -161,7 +161,7 @@
           "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");
+          "Change has been successfully merged by Administrator");
     }
   }
 
@@ -253,9 +253,10 @@
     assertThat(c.revisions.get(expectedId.name())._number).isEqualTo(expectedNum);
     try (Repository repo =
         repoManager.openRepository(new Project.NameKey(c.project))) {
-      Ref ref = repo.getRef(
-          new PatchSet.Id(new Change.Id(c._number), expectedNum).toRefName());
-      assertThat(ref).isNotNull();
+      String refName = new PatchSet.Id(new Change.Id(c._number), expectedNum)
+          .toRefName();
+      Ref ref = repo.getRef(refName);
+      assertThat(ref).named(refName).isNotNull();
       assertThat(ref.getObjectId()).isEqualTo(expectedId);
     }
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
index f42a134..1a8e151 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -5,10 +5,11 @@
   'AbstractSubmitByMerge.java',
 ]
 
-SUBMIT_TESTS = glob(['Submit*IT.java'], excludes = SUBMIT_UTIL_SRCS)
-OTHER_TESTS = glob(['*IT.java'], excludes = SUBMIT_TESTS + SUBMIT_UTIL_SRCS)
+SUBMIT_TESTS = glob(['Submit*IT.java'])
+OTHER_TESTS = glob(['*IT.java'], excludes = SUBMIT_TESTS)
 
 acceptance_tests(
+  group = 'rest-change-other',
   srcs = OTHER_TESTS,
   deps = [
     ':submit_util',
@@ -16,8 +17,9 @@
   ],
   labels = ['rest'],
 )
-
+ 
 acceptance_tests(
+  group = 'rest-change-submit',
   srcs = SUBMIT_TESTS,
   deps = [
     ':submit_util',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index a56a7f2..d726d70 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
@@ -22,10 +22,8 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.ConfigSuite;
 
-import org.eclipse.jgit.lib.Config;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeUtils;
 import org.joda.time.DateTimeUtils.MillisProvider;
@@ -42,11 +40,6 @@
   private String systemTimeZone;
   private volatile long clockStepMs;
 
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
-
   @Before
   public void setTimeForTesting() {
     systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
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 6ef53ff..aa7305a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.common.collect.Sets;
 import com.google.common.truth.IterableSubject;
@@ -22,17 +23,15 @@
 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 org.eclipse.jgit.lib.Config;
+import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class HashtagsIT extends AbstractDaemonTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return NotesMigration.allEnabledConfig();
+  @Before
+  public void before() {
+    assume().that(notesMigration.enabled()).isTrue();
   }
 
   @Test
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 2af3c25..a87b7d9 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
@@ -49,8 +49,10 @@
     PushOneCommit.Result change = createChange();
     PushOneCommit.Result change2 = createChange();
 
-    approve(change.getChangeId());
-    submit(change2.getChangeId());
+    String id1 = change.getChangeId();
+    String id2 = change2.getChangeId();
+    approve(id1);
+    submit(id2);
 
     RevCommit head = getRemoteHead();
     assertThat(head.getId()).isEqualTo(change2.getCommitId());
@@ -59,6 +61,8 @@
     assertSubmitter(change2.getChangeId(), 1);
     assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
     assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertSubmittedTogether(id1, id2, id1);
+    assertSubmittedTogether(id2, id2, id1);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
index c89da30..0802e7c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-config',
   srcs = glob(['*IT.java']),
   labels = ['rest']
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
index ffdfa8b..d991417 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-group',
   srcs = glob(['*IT.java']),
   labels = ['rest']
 )
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 50e6b84..c1618fb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-project',
   srcs = glob(['*IT.java']),
   deps = [
     ':branch',
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 1b79bc3..6617127 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
@@ -22,7 +22,7 @@
 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.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
 import org.junit.Test;
@@ -155,7 +155,7 @@
         list().withRegex(".*ast.*r").get());
   }
 
-  private ListBranchesRequest list() throws Exception {
+  private ListRefsRequest<BranchInfo> list() throws Exception {
     return gApi.projects().name(project.get()).branches();
   }
 
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 7efefa7..e908675 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -16,27 +16,46 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.common.TagInfo;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
 import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.PushCommand;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.junit.Test;
 
 import java.util.List;
 
 public class TagsIT extends AbstractDaemonTest {
+  private static final List<String> testTags = ImmutableList.of(
+      "tag-A", "tag-B", "tag-C", "tag-D", "tag-E", "tag-F", "tag-G", "tag-H");
+
   @Test
-  public void listTagsOfNonExistingProject_NotFound() throws Exception {
+  public void listTagsOfNonExistingProject() throws Exception {
     assertThat(adminSession.get("/projects/non-existing/tags").getStatusCode())
         .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void listTagsOfNonVisibleProject_NotFound() throws Exception {
+  public void listTagsOfNonExistingProjectWithApi() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name("does-not-exist").tags();
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name("does-not-exist").tag("tag").get();
+  }
+
+  @Test
+  public void listTagsOfNonVisibleProject() throws Exception {
     blockRead(project, "refs/*");
     assertThat(
         userSession.get("/projects/" + project.get() + "/tags").getStatusCode())
@@ -44,6 +63,15 @@
   }
 
   @Test
+  public void listTagsOfNonVisibleProjectWithApi() throws Exception {
+    blockRead(project, "refs/*");
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(project.get()).tags();
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(project.get()).tag("tag").get();
+  }
+
+  @Test
   public void listTags() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     grant(Permission.CREATE, project, "refs/tags/*");
@@ -62,20 +90,88 @@
     PushOneCommit.Result r2 = push2.to("refs/for/master%submit");
     r2.assertOkStatus();
 
-    List<TagInfo> result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
-    assertThat(result).hasSize(2);
+    String tag3Ref = Constants.R_TAGS + "vLatest";
+    PushCommand pushCmd = testRepo.git().push();
+    pushCmd.setRefSpecs(new RefSpec(tag2.name + ":" + tag3Ref));
+    Iterable<PushResult> r = pushCmd.call();
+    assertThat(Iterables.getOnlyElement(r).getRemoteUpdate(tag3Ref).getStatus())
+        .isEqualTo(Status.OK);
+
+    List<TagInfo> result = getTags().get();
+    assertThat(result).hasSize(3);
 
     TagInfo t = result.get(0);
-    assertThat(t.ref).isEqualTo("refs/tags/" + tag1.name);
+    assertThat(t.ref).isEqualTo(Constants.R_TAGS + tag1.name);
     assertThat(t.revision).isEqualTo(r1.getCommitId().getName());
 
     t = result.get(1);
-    assertThat(t.ref).isEqualTo("refs/tags/" + tag2.name);
+    assertThat(t.ref).isEqualTo(Constants.R_TAGS + tag2.name);
     assertThat(t.object).isEqualTo(r2.getCommitId().getName());
     assertThat(t.message).isEqualTo(tag2.message);
     assertThat(t.tagger.name).isEqualTo(tag2.tagger.getName());
     assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
+
+    t = result.get(2);
+    assertThat(t.ref).isEqualTo(tag3Ref);
+    assertThat(t.object).isEqualTo(r2.getCommitId().getName());
+    assertThat(t.message).isEqualTo(tag2.message);
+    assertThat(t.tagger.name).isEqualTo(tag2.tagger.getName());
+    assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
+  }
+
+  private void assertTagList(FluentIterable<String> expected, List<TagInfo> actual)
+      throws Exception {
+    assertThat(actual).hasSize(expected.size());
+    for (int i = 0; i < expected.size(); i ++) {
+      assertThat(actual.get(i).ref).isEqualTo("refs/tags/" + expected.get(i));
+    }
+  }
+
+  @Test
+  public void listTagsWithoutOptions() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().get();
+    assertTagList(FluentIterable.from(testTags), result);
+  }
+
+  @Test
+  public void listTagsWithStartOption() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withStart(1).get();
+    assertTagList(FluentIterable.from(testTags).skip(1), result);
+  }
+
+  @Test
+  public void listTagsWithLimitOption() throws Exception {
+    createTags();
+    int limit = testTags.size() - 1;
+    List<TagInfo> result = getTags().withLimit(limit).get();
+    assertTagList(FluentIterable.from(testTags).limit(limit), result);
+  }
+
+  @Test
+  public void listTagsWithLimitAndStartOption() throws Exception {
+    createTags();
+    int limit = testTags.size() - 3;
+    List<TagInfo> result = getTags().withStart(1).withLimit(limit).get();
+    assertTagList(FluentIterable.from(testTags).skip(1).limit(limit), result);
+  }
+
+  @Test
+  public void listTagsWithRegexFilter() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withRegex("^tag-[C|D]$").get();
+    assertTagList(
+        FluentIterable.from(ImmutableList.of("tag-C", "tag-D")), result);
+  }
+
+  @Test
+  public void listTagsWithSubstringFilter() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withSubstring("tag-").get();
+    assertTagList(FluentIterable.from(testTags), result);
+    result = getTags().withSubstring("ag-B").get();
+    assertTagList(FluentIterable.from(ImmutableList.of("tag-B")), result);
   }
 
   @Test
@@ -98,8 +194,7 @@
     PushOneCommit.Result r2 = push2.to("refs/for/hidden%submit");
     r2.assertOkStatus();
 
-    List<TagInfo> result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    List<TagInfo> result = getTags().get();
     assertThat(result).hasSize(2);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
@@ -107,8 +202,7 @@
     assertThat(result.get(1).revision).isEqualTo(r2.getCommitId().getName());
 
     blockRead(project, "refs/heads/hidden");
-    result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    result = getTags().get();
     assertThat(result).hasSize(1);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
@@ -126,18 +220,29 @@
     PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
     r1.assertOkStatus();
 
-    RestResponse response =
-        adminSession.get("/projects/" + project.get() + "/tags/" + tag1.name);
-    TagInfo tagInfo =
-        newGson().fromJson(response.getReader(), TagInfo.class);
+    TagInfo tagInfo = getTag(tag1.name);
     assertThat(tagInfo.ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(tagInfo.revision).isEqualTo(r1.getCommitId().getName());
   }
 
-  private static List<TagInfo> toTagInfoList(RestResponse r) throws Exception {
-    List<TagInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<List<TagInfo>>() {}.getType());
-    return result;
+  private void createTags() throws Exception {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
+    for (String tagname : testTags) {
+      PushOneCommit.Tag tag = new PushOneCommit.Tag(tagname);
+      PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+      push.setTag(tag);
+      PushOneCommit.Result result = push.to("refs/for/master%submit");
+      result.assertOkStatus();
+    }
+  }
+
+  private ListRefsRequest<TagInfo> getTags() throws Exception {
+    return gApi.projects().name(project.get()).tags();
+  }
+
+  private TagInfo getTag(String ref) throws Exception {
+    return gApi.projects().name(project.get()).tag(ref).get();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
index fce853b..94e69da 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'server-change',
   srcs = glob(['*IT.java']),
   labels = ['server'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 721c712..5592755 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
@@ -39,14 +39,11 @@
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Revisions;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -57,11 +54,6 @@
 
 @NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
-
   @Inject
   private Provider<ChangesCollection> changes;
 
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 9a88f56..42f3fe7 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
@@ -20,7 +20,6 @@
 import com.google.common.collect.ImmutableList;
 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.RestSession;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -75,38 +74,41 @@
 
   @Test
   public void getRelatedLinear() throws Exception {
+    // 1,1---2,1
     RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
         .message("subject: 1")
         .create();
-    String id1 = getChangeId(c1_1);
-    RevCommit c2_2 = commitBuilder()
+    RevCommit c2_1 = commitBuilder()
         .add("b.txt", "2")
         .message("subject: 2")
         .create();
-    String id2 = getChangeId(c2_2);
     pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
 
-    for (RevCommit c : ImmutableList.of(c2_2, c1_1)) {
-      assertRelated(getPatchSetId(c),
-          changeAndCommit(id2, c2_2, 1, 1),
-          changeAndCommit(id1, c1_1, 1, 1));
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
     }
   }
 
   @Test
   public void getRelatedReorder() throws Exception {
+    // 1,1---2,1
+    //
+    // 2,2---1,2
+
     // Create two commits and push.
     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);
@@ -121,31 +123,71 @@
 
     for (PatchSet.Id ps : ImmutableList.of(ps2_2, ps1_2)) {
       assertRelated(ps,
-          changeAndCommit(id1, c1_2, 2, 2),
-          changeAndCommit(id2, c2_2, 2, 2));
+          changeAndCommit(ps1_2, c1_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2));
     }
 
     for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
       assertRelated(ps,
-          changeAndCommit(id2, c2_1, 1, 2),
-          changeAndCommit(id1, c1_1, 1, 2));
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
     }
   }
 
   @Test
+  public void getRelatedAmendParentChange() throws Exception {
+    // 1,1---2,1
+    //
+    // 1,2
+
+    // Create two commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+
+    // Amend parent change and push.
+    testRepo.reset("HEAD~1");
+    RevCommit c1_2 = amendBuilder()
+        .add("c.txt", "2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+
+    assertRelated(ps1_2,
+        changeAndCommit(ps2_1, c2_1, 1),
+        changeAndCommit(ps1_2, c1_2, 2));
+  }
+
+  @Test
   public void getRelatedReorderAndExtend() throws Exception {
+    // 1,1---2,1
+    //
+    // 2,2---1,2---3,1
+
     // Create two commits and push.
     ObjectId initial = repo().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);
@@ -158,7 +200,6 @@
         .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);
@@ -166,36 +207,248 @@
 
     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));
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps1_2, c1_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2));
     }
 
     for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
       assertRelated(ps,
-          changeAndCommit(id3, c3_1, 1, 1),
-          changeAndCommit(id2, c2_1, 1, 2),
-          changeAndCommit(id1, c1_1, 1, 2));
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+  }
+
+  @Test
+  public void getRelatedReworkSeries() throws Exception {
+    // 1,1---2,1---3,1
+    //
+    // 1,2---2,2---3,2
+
+    // Create three commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 2")
+        .create();
+    RevCommit c3_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    // Amend all changes change and push.
+    testRepo.reset(c1_1);
+    RevCommit c1_2 = amendBuilder()
+        .add("a.txt", "2")
+        .create();
+    RevCommit c2_2 = commitBuilder()
+        .add("b.txt", "2")
+        .message(parseBody(c2_1).getFullMessage())
+        .create();
+    RevCommit c3_2 = commitBuilder()
+        .add("b.txt", "3")
+        .message(parseBody(c3_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_2);
+    PatchSet.Id ps3_2 = getPatchSetId(c3_2);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_1, ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 2),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_2, ps2_2, ps3_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_2, c3_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2),
+          changeAndCommit(ps1_2, c1_2, 2));
+    }
+  }
+
+  @Test
+  public void getRelatedReworkThenExtendInTheMiddleOfSeries() throws Exception {
+    // 1,1---2,1---3,1
+    //
+    // 1,2---2,2---3,2
+    //   \---4,1
+
+    // Create three commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 2")
+        .create();
+    RevCommit c3_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    // Amend all changes change and push.
+    testRepo.reset(c1_1);
+    RevCommit c1_2 = amendBuilder()
+        .add("a.txt", "2")
+        .create();
+    RevCommit c2_2 = commitBuilder()
+        .add("b.txt", "2")
+        .message(parseBody(c2_1).getFullMessage())
+        .create();
+    RevCommit c3_2 = commitBuilder()
+        .add("b.txt", "3")
+        .message(parseBody(c3_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_2);
+    PatchSet.Id ps3_2 = getPatchSetId(c3_2);
+
+    // Add one more commit 4,1 based on 1,2.
+    testRepo.reset(c1_2);
+    RevCommit c4_1 = commitBuilder()
+        .add("d.txt", "4")
+        .message("subject: 4")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps4_1 = getPatchSetId(c4_1);
+
+    // 1,1 is related indirectly to 4,1.
+    assertRelated(ps1_1,
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps3_1, c3_1, 2),
+        changeAndCommit(ps2_1, c2_1, 2),
+        changeAndCommit(ps1_1, c1_1, 2));
+
+    // 2,1 and 3,1 don't include 4,1 since we don't walk forward after walking
+    // backward.
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 2),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+
+    // 1,2 is related directly to 4,1, and the 2-3 parallel branch stays intact.
+    assertRelated(ps1_2,
+        changeAndCommit(ps3_2, c3_2, 2),
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps2_2, c2_2, 2),
+        changeAndCommit(ps1_2, c1_2, 2));
+
+    // 4,1 is only related to 1,2, since we don't walk forward after walking
+    // backward.
+    assertRelated(ps4_1,
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps1_2, c1_2, 2));
+
+    // 2,2 and 3,2 don't include 4,1 since we don't walk forward after walking
+    // backward.
+    for (PatchSet.Id ps : ImmutableList.of(ps2_2, ps3_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_2, c3_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2),
+          changeAndCommit(ps1_2, c1_2, 2));
+    }
+  }
+
+  @Test
+  public void getRelatedCrissCrossDependency() throws Exception {
+    // 1,1---2,1---3,2
+    //
+    // 1,2---2,2---3,1
+
+    // Create two commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+
+    // Amend both changes change and push.
+    testRepo.reset(c1_1);
+    RevCommit c1_2 = amendBuilder()
+        .add("a.txt", "2")
+        .create();
+    RevCommit c2_2 = commitBuilder()
+        .add("b.txt", "2")
+        .message(parseBody(c2_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_2);
+
+    // PS 3,1 depends on 2,2.
+    RevCommit c3_1 = commitBuilder()
+        .add("c.txt", "1")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    // PS 3,2 depends on 2,1.
+    testRepo.reset(c2_1);
+    RevCommit c3_2 = commitBuilder()
+        .add("c.txt", "2")
+        .message(parseBody(c3_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps3_2 = getPatchSetId(c3_2);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_1, ps2_1, ps3_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_2, c3_2, 2),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_2, ps2_2, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 2),
+          changeAndCommit(ps2_2, c2_2, 2),
+          changeAndCommit(ps1_2, c1_2, 2));
     }
   }
 
   @Test
   public void getRelatedEdit() throws Exception {
+    // 1,1---2,1---3,1
+    //   \---2,E---/
+
     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_1).change();
@@ -212,37 +465,38 @@
 
     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));
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
     }
 
     assertRelated(ps2_edit,
-        changeAndCommit(id3, c3_1, 1, 1),
-        changeAndCommit(id2, editRev, 0, 1),
-        changeAndCommit(id1, c1_1, 1, 1));
+        changeAndCommit(ps3_1, c3_1, 1),
+        changeAndCommit(new PatchSet.Id(ch2.getId(), 0), editRev, 1),
+        changeAndCommit(ps1_1, c1_1, 1));
   }
 
   @Test
   public void pushNewPatchSetWhenParentHasNullGroup() throws Exception {
+    // 1,1---2,1
+    //   \---2,2
+
     RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
         .message("subject: 1")
         .create();
-    String id1 = getChangeId(c1_1);
     RevCommit c2_1 = commitBuilder()
         .add("b.txt", "2")
         .message("subject: 2")
         .create();
-    String id2 = getChangeId(c2_1);
     pushHead(testRepo, "refs/for/master", false);
     PatchSet.Id psId1_1 = getPatchSetId(c1_1);
     PatchSet.Id psId2_1 = getPatchSetId(c2_1);
 
     for (PatchSet.Id psId : ImmutableList.of(psId1_1, psId2_1)) {
       assertRelated(psId,
-          changeAndCommit(id2, c2_1, 1, 1),
-          changeAndCommit(id1, c1_1, 1, 1));
+          changeAndCommit(psId2_1, c2_1, 1),
+          changeAndCommit(psId1_1, c1_1, 1));
     }
 
     // Pretend PS1,1 was pushed before the groups field was added.
@@ -266,8 +520,8 @@
     // Push updated the group for PS1,1, so it shows up in related changes even
     // though a new patch set was not pushed.
     assertRelated(psId2_2,
-        changeAndCommit(id2, c2_2, 2, 2),
-        changeAndCommit(id1, c1_1, 1, 1));
+        changeAndCommit(psId2_2, c2_2, 2),
+        changeAndCommit(psId1_1, c1_1, 1));
   }
 
   private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws IOException {
@@ -282,8 +536,9 @@
         RelatedInfo.class).changes;
   }
 
-  private String getChangeId(RevCommit c) throws Exception {
-    return GitUtil.getChangeId(testRepo, c).get();
+  private RevCommit parseBody(RevCommit c) throws IOException {
+    testRepo.getRevWalk().parseBody(c);
+    return c;
   }
 
   private PatchSet.Id getPatchSetId(ObjectId c) throws OrmException {
@@ -298,13 +553,13 @@
     return Iterables.getOnlyElement(queryProvider.get().byCommit(c));
   }
 
-  private static ChangeAndCommit changeAndCommit(String changeId,
-      ObjectId commitId, int revisionNum, int currentRevisionNum) {
+  private static ChangeAndCommit changeAndCommit(
+      PatchSet.Id psId, ObjectId commitId, int currentRevisionNum) {
     ChangeAndCommit result = new ChangeAndCommit();
-    result.changeId = changeId;
+    result._changeNumber = psId.getParentKey().get();
     result.commit = new CommitInfo();
     result.commit.commit = commitId.name();
-    result._revisionNumber = revisionNum;
+    result._revisionNumber = psId.get();
     result._currentRevisionNumber = currentRevisionNum;
     result.status = "NEW";
     return result;
@@ -313,18 +568,18 @@
   private void assertRelated(PatchSet.Id psId, ChangeAndCommit... expected)
       throws Exception {
     List<ChangeAndCommit> actual = getRelated(psId);
-    assertThat(actual).hasSize(expected.length);
+    assertThat(actual).named("related to " + psId).hasSize(expected.length);
     for (int i = 0; i < actual.size(); i++) {
       String name = "index " + i + " related to " + psId;
       ChangeAndCommit a = actual.get(i);
       ChangeAndCommit e = expected[i];
-      assertThat(a.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._changeNumber).named("change ID of " + name)
+          .isEqualTo(e._changeNumber);
+      // Don't bother checking changeId; assume _changeNumber is sufficient.
       assertThat(a._revisionNumber).named("revision of " + name)
           .isEqualTo(e._revisionNumber);
+      assertThat(a.commit.commit).named("commit of " + name)
+          .isEqualTo(e.commit.commit);
       assertThat(a._currentRevisionNumber).named("current revision of " + name)
           .isEqualTo(e._currentRevisionNumber);
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 6ac34ce..5a8a44c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -17,14 +17,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
-import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.ProjectConfig;
 
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
@@ -33,8 +31,6 @@
 import org.junit.Test;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
 
 public class SubmittedTogetherIT extends AbstractDaemonTest {
 
@@ -145,11 +141,8 @@
   }
 
   @Test
+  @TestProjectInput(submitType = SubmitType.CHERRY_PICK)
   public void testCherryPickWithoutAncestors() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    cfg.getProject().setSubmitType(SubmitType.CHERRY_PICK);
-    saveProjectConfig(project, cfg);
-
     // Create two commits and push.
     RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
@@ -167,18 +160,35 @@
     assertSubmittedTogether(id2);
   }
 
-  private void assertSubmittedTogether(String chId, String... expected)
-      throws Exception {
-    List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
-    assertThat(actual).hasSize(expected.length);
-    assertThat(Arrays.asList(expected))
-        .containsExactlyElementsIn(
-            Iterables.transform(actual, new Function<ChangeInfo, String>() {
-              @Override
-              public String apply(ChangeInfo input) {
-                return input.changeId;
-              }
-            })).inOrder();
+  @Test
+  public void testSubmissionIdSavedOnMergeInOneProject() throws Exception {
+    // Create two commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = getChangeId(c2_1);
+    pushHead(testRepo, "refs/for/master", false);
+
+    assertSubmittedTogether(id1);
+    assertSubmittedTogether(id2, id2, id1);
+
+    approve(id1);
+    approve(id2);
+    submit(id2);
+    assertMerged(id1);
+    assertMerged(id2);
+
+    // Prior to submission this was empty, but the post-merge value is what was
+    // actually submitted.
+    assertSubmittedTogether(id1, id2, id1);
+
+    assertSubmittedTogether(id2, id2, id1);
   }
 
   private RevCommit getRemoteHead() throws IOException {
@@ -191,4 +201,19 @@
   private String getChangeId(RevCommit c) throws Exception {
     return GitUtil.getChangeId(testRepo, c).get();
   }
-}
\ No newline at end of file
+
+  private void submit(String changeId) throws Exception {
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit();
+  }
+
+  private void assertMerged(String changeId) throws Exception {
+    assertThat(gApi
+        .changes()
+        .id(changeId)
+        .get()
+        .status).isEqualTo(ChangeStatus.MERGED);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
index 2ec6f7a..ad7d597 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
@@ -3,11 +3,13 @@
 FLAKY_TEST_CASES=['ProjectWatchIT.java']
 
 acceptance_tests(
+  group = 'server-project',
   srcs = glob(['*IT.java'], excludes=FLAKY_TEST_CASES),
   labels = ['server'],
 )
 
 acceptance_tests(
+  group = 'server-project-flaky',
   srcs = FLAKY_TEST_CASES,
   labels = ['server', 'flaky'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index da83157..357f268 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -24,6 +24,7 @@
 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.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
@@ -43,6 +44,10 @@
       value(0, "No score"),
       value(-1, "Negative"));
 
+  private final LabelType P = category("CustomLabel2",
+      value(1, "Positive"),
+      value(0, "No score"));
+
   @Before
   public void setUp() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
@@ -50,6 +55,8 @@
         SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
     Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers,
         "refs/heads/*");
+    Util.allow(cfg, Permission.forLabel(P.getName()), 0, 1, anonymousUsers,
+        "refs/heads/*");
     saveProjectConfig(project, cfg);
   }
 
@@ -107,6 +114,26 @@
   }
 
   @Test
+  public void customLabelAnyWithBlock_Addreviewer_ZeroVote() throws Exception {
+    P.setFunctionName("AnyWithBlock");
+    saveLabelConfig();
+    PushOneCommit.Result r = createChange();
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+
+    revision(r).review(new ReviewInput().label(P.getName(), 0));
+    ChangeInfo c = get(r.getChangeId());
+    LabelInfo q = c.labels.get(P.getName());
+    assertThat(q.all).hasSize(2);
+    assertThat(q.disliked).isNull();
+    assertThat(q.rejected).isNull();
+    assertThat(q.blocking).isNull();
+  }
+
+  @Test
   public void customLabelMaxWithBlock_NegativeVoteBlock() throws Exception {
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
@@ -122,6 +149,7 @@
   private void saveLabelConfig() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     cfg.getLabelSections().put(label.getName(), label);
+    cfg.getLabelSections().put(P.getName(), P);
     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 5d31c77..363a7e4 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
@@ -26,22 +26,14 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.Util;
-import com.google.gerrit.testutil.ConfigSuite;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class LabelTypeIT extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
-
   private LabelType codeReview;
 
   @Before
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 d067b34..0729b68 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'ssh',
   srcs = glob(['*IT.java']),
   deps = ['//lib/commons:compress'],
   labels = ['ssh'],
diff --git a/gerrit-acceptance-tests/tests.defs b/gerrit-acceptance-tests/tests.defs
index 7bd2430..940b1cc 100644
--- a/gerrit-acceptance-tests/tests.defs
+++ b/gerrit-acceptance-tests/tests.defs
@@ -5,6 +5,7 @@
 ]
 
 def acceptance_tests(
+    group,
     srcs,
     deps = [],
     labels = [],
@@ -16,19 +17,18 @@
   if path.exists('/dev/urandom'):
     vm_args = vm_args + ['-Djava.security.egd=file:/dev/./urandom']
 
-  for j in srcs:
-    java_test(
-      name = j[:-len('.java')],
-      srcs = [j],
-      deps = ['//gerrit-acceptance-tests:lib'] + deps,
-      source_under_test = [
-        '//gerrit-httpd:httpd',
-        '//gerrit-sshd:sshd',
-        '//gerrit-server:server',
-      ] + source_under_test,
-      labels = labels + [
-        'acceptance',
-        'slow',
-      ],
-      vm_args = vm_args,
-    )
+  java_test(
+    name = group,
+    srcs = srcs,
+    deps = ['//gerrit-acceptance-tests:lib'] + deps,
+    source_under_test = [
+      '//gerrit-httpd:httpd',
+      '//gerrit-sshd:sshd',
+      '//gerrit-server:server',
+    ] + source_under_test,
+    labels = labels + [
+      'acceptance',
+      'slow',
+    ],
+    vm_args = vm_args,
+  )
diff --git a/gerrit-antlr/BUCK b/gerrit-antlr/BUCK
index 03c3c1e..e858a72 100644
--- a/gerrit-antlr/BUCK
+++ b/gerrit-antlr/BUCK
@@ -25,7 +25,6 @@
 genrule(
   name = 'query_link',
   cmd = 'ln -s $(location :lib) $OUT',
-  deps = [':lib'],
   out = 'query_parser.jar',
 )
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/FormatUtil.java
similarity index 62%
copy from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
copy to gerrit-common/src/main/java/com/google/gerrit/common/FormatUtil.java
index cd01186..0f6b37a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/FormatUtil.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.common;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
+public class FormatUtil {
+  public static String elide(String s, int max) {
+    if (s == null || s.length() <= max) {
+      return s;
+    }
+    int len = (max - 3) / 2;
+    return s.substring(0, len) + "..." + s.substring(s.length() - len);
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index 9a30696..9bc2ea5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -26,6 +26,7 @@
 import java.net.URLClassLoader;
 import java.nio.file.Path;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Set;
 
 public final class IoUtil {
@@ -86,8 +87,8 @@
     }
   }
 
-  public static void loadJARs(Path... jars) {
-    loadJARs(Arrays.asList(jars));
+  public static void loadJARs(Path jar) {
+    loadJARs(Collections.singleton(jar));
   }
 
   private static UnsupportedOperationException noAddURL(String m, Throwable why) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index ff2121d..28e0d24 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -25,6 +25,8 @@
 public class PageLinks {
   public static final String SETTINGS = "/settings/";
   public static final String SETTINGS_PREFERENCES = "/settings/preferences";
+  public static final String SETTINGS_DIFF_PREFERENCES = "/settings/diff-preferences";
+  public static final String SETTINGS_EDIT_PREFERENCES = "/settings/edit-preferences";
   public static final String SETTINGS_SSHKEYS = "/settings/ssh-keys";
   public static final String SETTINGS_GPGKEYS = "/settings/gpg-keys";
   public static final String SETTINGS_HTTP_PASSWORD = "/settings/http-password";
@@ -89,6 +91,10 @@
     return "/admin/projects/" + p.get() + ",branches";
   }
 
+  public static String toProjectTags(Project.NameKey p) {
+    return "/admin/projects/" + p.get() + ",tags";
+  }
+
   public static String toAccountQuery(String fullname, Status status) {
     return toChangeQuery(op("owner", fullname) + " " + status(status));
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
index 9f4da74..39f5cb0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 
-import java.util.Collections;
 import java.util.List;
 
 public class PatchSetDetail {
@@ -27,7 +26,6 @@
   protected PatchSetInfo info;
   protected List<Patch> patches;
   protected Project.NameKey project;
-  protected List<UiCommandDetail> commands;
 
   public PatchSetDetail() {
   }
@@ -63,15 +61,4 @@
   public void setProject(final Project.NameKey p) {
     project = p;
   }
-
-  public List<UiCommandDetail> getCommands() {
-    if (commands != null) {
-      return commands;
-    }
-    return Collections.emptyList();
-  }
-
-  public void setCommands(List<UiCommandDetail> cmds) {
-    commands = cmds.isEmpty() ? null : cmds;
-  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
index bf84ccb0..c007161 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
@@ -22,4 +22,11 @@
   @DefaultInput
   public Set<String> add;
   public Set<String> remove;
+
+  public HashtagsInput(){
+  }
+
+  public HashtagsInput(Set<String> add) {
+    this.add = add;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
index ab09e5f..b909f31 100644
--- 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
@@ -63,6 +63,7 @@
     private int limit;
     private int start;
     private String substring;
+    private String suggest;
 
     public List<GroupInfo> get() throws RestApiException {
       Map<String, GroupInfo> map = getAsMap();
@@ -128,6 +129,11 @@
       return this;
     }
 
+    public ListRequest withSuggest(String suggest) {
+      this.suggest = suggest;
+      return this;
+    }
+
     public EnumSet<ListGroupsOption> getOptions() {
       return options;
     }
@@ -163,5 +169,9 @@
     public String getSubstring() {
       return substring;
     }
+
+    public String getSuggest() {
+      return suggest;
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
index b973806..77513a2 100644
--- 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
@@ -20,9 +20,7 @@
 import java.util.List;
 import java.util.Map;
 
-public class BranchInfo {
-  public String ref;
-  public String revision;
+public class BranchInfo extends RefInfo {
   public Boolean canDelete;
   public Map<String, ActionInfo> actions;
   public List<WebLinkInfo> webLinks;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 102b1ce..e3eb4be 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -28,32 +28,33 @@
   String description() throws RestApiException;
   void description(PutDescriptionInput in) throws RestApiException;
 
-  ListBranchesRequest branches();
+  ListRefsRequest<BranchInfo> branches();
+  ListRefsRequest<TagInfo> tags();
 
-  public abstract class ListBranchesRequest {
-    private int limit;
-    private int start;
-    private String substring;
-    private String regex;
+  public abstract class ListRefsRequest<T extends RefInfo> {
+    protected int limit;
+    protected int start;
+    protected String substring;
+    protected String regex;
 
-    public abstract List<BranchInfo> get() throws RestApiException;
+    public abstract List<T> get() throws RestApiException;
 
-    public ListBranchesRequest withLimit(int limit) {
+    public ListRefsRequest<T> withLimit(int limit) {
       this.limit = limit;
       return this;
     }
 
-    public ListBranchesRequest withStart(int start) {
+    public ListRefsRequest<T> withStart(int start) {
       this.start = start;
       return this;
     }
 
-    public ListBranchesRequest withSubstring(String substring) {
+    public ListRefsRequest<T> withSubstring(String substring) {
       this.substring = substring;
       return this;
     }
 
-    public ListBranchesRequest withRegex(String regex) {
+    public ListRefsRequest<T> withRegex(String regex) {
       this.regex = regex;
       return this;
     }
@@ -73,7 +74,6 @@
     public String getRegex() {
       return regex;
     }
-
   }
 
   List<ProjectInfo> children() throws RestApiException;
@@ -96,6 +96,15 @@
   BranchApi branch(String ref) throws RestApiException;
 
   /**
+   * Look up a tag by refname.
+   * <p>
+   * @param ref tag name, with or without "refs/tags/" prefix.
+   * @throws RestApiException if a problem occurred reading the project.
+   * @return API for accessing the tag.
+   */
+  TagApi tag(String ref) throws RestApiException;
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
@@ -127,7 +136,12 @@
     }
 
     @Override
-    public ListBranchesRequest branches() {
+    public ListRefsRequest<BranchInfo> branches() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ListRefsRequest<TagInfo> tags() {
       throw new NotImplementedException();
     }
 
@@ -150,5 +164,10 @@
     public BranchApi branch(String ref) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public TagApi tag(String ref) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
similarity index 65%
rename from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
index cd01186..1844a76 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.extensions.api.projects;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
+public class RefInfo {
+  public String ref;
+  public String revision;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
new file mode 100644
index 0000000..6cc1ba4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface TagApi {
+  TagInfo get() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements TagApi {
+    @Override
+    public TagInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
similarity index 87%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index 3e3d8db..b531d67 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.api.projects;
 
-public class TagInfo {
-  public String ref;
-  public String revision;
+import com.google.gerrit.extensions.common.GitPerson;
+
+public class TagInfo extends RefInfo {
   public String object;
   public String message;
   public GitPerson tagger;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
new file mode 100644
index 0000000..3e45523
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.client;
+
+/* This class is stored in Git config file. */
+public class EditPreferencesInfo {
+  public Integer tabSize;
+  public Integer lineLength;
+  public Integer cursorBlinkRate;
+  public Boolean hideTopMenu;
+  public Boolean showTabs;
+  public Boolean showWhitespaceErrors;
+  public Boolean syntaxHighlighting;
+  public Boolean hideLineNumbers;
+  public Boolean matchBrackets;
+  public Boolean autoCloseBrackets;
+  public Theme theme;
+  public KeyMapType keyMapType;
+
+  public static EditPreferencesInfo defaults() {
+    EditPreferencesInfo i = new EditPreferencesInfo();
+    i.tabSize = 8;
+    i.lineLength = 100;
+    i.cursorBlinkRate = 0;
+    i.hideTopMenu = false;
+    i.showTabs = true;
+    i.showWhitespaceErrors = false;
+    i.syntaxHighlighting = true;
+    i.hideLineNumbers = false;
+    i.matchBrackets = true;
+    i.autoCloseBrackets = false;
+    i.theme = Theme.DEFAULT;
+    i.keyMapType = KeyMapType.DEFAULT;
+    return i;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
similarity index 65%
copy from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
index cd01186..261168d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.extensions.client;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
-}
+public enum KeyMapType {
+  DEFAULT,
+  EMACS,
+  VIM
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index c1edd6a..7e99d1a 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
@@ -59,7 +59,10 @@
   CHANGE_ACTIONS(16),
 
   /** Include a copy of commit messages including review footers. */
-  COMMIT_FOOTERS(17);
+  COMMIT_FOOTERS(17),
+
+  /** Include push certificate information along with any patch sets. */
+  PUSH_CERTIFICATES(18);
 
   private final int value;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
index 58f5494..00d0c18 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -20,4 +20,5 @@
   public String oldPath;
   public Integer linesInserted;
   public Integer linesDeleted;
+  public long sizeDelta;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GpgKeyInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
index 443ef07..33adbea 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
@@ -17,8 +17,33 @@
 import java.util.List;
 
 public class GpgKeyInfo {
+  /**
+   * Status of checking an object like a key or signature.
+   * <p>
+   * Order of values in this enum is significant: OK is "better" than BAD, etc.
+   */
+  public enum Status {
+    /** Something is wrong with this key. */
+    BAD,
+
+    /**
+     * Inspecting only this key found no problems, but the system does not fully
+     * trust the key's origin.
+     */
+    OK,
+
+    /**
+     * This key is valid, and the system knows enough about the key and its
+     * origin to trust it.
+     */
+    TRUSTED;
+  }
+
   public String id;
   public String fingerprint;
   public List<String> userIds;
   public String key;
+
+  public Status status;
+  public List<String> problems;
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
similarity index 65%
copy from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
index cd01186..9eed808 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.extensions.common;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
+public class PushCertificateInfo {
+  public String certificate;
+  public GpgKeyInfo key;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
index bc0fa6d..025c623 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -29,4 +29,5 @@
   public Map<String, FileInfo> files;
   public Map<String, ActionInfo> actions;
   public String commitWithFooters;
+  public PushCertificateInfo pushCertificate;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 92fefed..a21c2d4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
@@ -23,7 +25,6 @@
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
 import java.nio.charset.UnsupportedCharsetException;
 
 /**
@@ -58,7 +59,7 @@
   }
 
   private String contentType = OCTET_STREAM;
-  private String characterEncoding;
+  private Charset characterEncoding;
   private long contentLength = -1;
   private boolean gzip = true;
   private boolean base64 = false;
@@ -66,9 +67,9 @@
 
   /** @return the MIME type of the result, for HTTP clients. */
   public String getContentType() {
-    String enc = getCharacterEncoding();
+    Charset enc = getCharacterEncoding();
     if (enc != null) {
-      return contentType + "; charset=" + enc;
+      return contentType + "; charset=" + enc.name();
     }
     return contentType;
   }
@@ -80,12 +81,18 @@
   }
 
   /** Get the character encoding; null if not known. */
-  public String getCharacterEncoding() {
+  public Charset getCharacterEncoding() {
     return characterEncoding;
   }
 
   /** Set the character set used to encode text data and return {@code this}. */
+  @Deprecated
   public BinaryResult setCharacterEncoding(String encoding) {
+    return setCharacterEncoding(Charset.forName(encoding));
+  }
+
+  /** Set the character set used to encode text data and return {@code this}. */
+  public BinaryResult setCharacterEncoding(Charset encoding) {
     characterEncoding = encoding;
     return this;
   }
@@ -183,11 +190,11 @@
         getContentType());
   }
 
-  private static String decode(byte[] data, String enc) {
+  private static String decode(byte[] data, Charset enc) {
     try {
       Charset cs = enc != null
-          ? Charset.forName(enc)
-          : StandardCharsets.UTF_8;
+          ? enc
+          : UTF_8;
       return cs.newDecoder()
         .onMalformedInput(CodingErrorAction.REPORT)
         .onUnmappableCharacter(CodingErrorAction.REPORT)
@@ -226,9 +233,9 @@
     private final String str;
 
     StringResult(String str) {
-      super(str.getBytes(StandardCharsets.UTF_8));
+      super(str.getBytes(UTF_8));
       setContentType("text/plain");
-      setCharacterEncoding("UTF-8");
+      setCharacterEncoding(UTF_8.name());
       this.str = str;
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
index 8f4d909..debfa20 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
@@ -40,7 +42,7 @@
   public static String encode(String component) {
     if (component != null) {
       try {
-        return URLEncoder.encode(component, "UTF-8");
+        return URLEncoder.encode(component, UTF_8.name());
       } catch (UnsupportedEncodingException e) {
         throw new RuntimeException("JVM must support UTF-8", e);
       }
@@ -52,7 +54,7 @@
   public static String decode(String str) {
     if (str != null) {
       try {
-        return URLDecoder.decode(str, "UTF-8");
+        return URLDecoder.decode(str, UTF_8.name());
       } catch (UnsupportedEncodingException e) {
         throw new RuntimeException("JVM must support UTF-8", e);
       }
diff --git a/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
index dc71b12..299b9b0 100644
--- a/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
+++ b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Key;
 import com.google.inject.util.Providers;
 
diff --git a/gerrit-gpg/BUCK b/gerrit-gpg/BUCK
index 592a5190..2b258a9 100644
--- a/gerrit-gpg/BUCK
+++ b/gerrit-gpg/BUCK
@@ -1,39 +1,36 @@
+DEPS = [
+  '//gerrit-common:server',
+  '//gerrit-extension-api:api',
+  '//gerrit-reviewdb:server',
+  '//gerrit-server:server',
+  '//lib:guava',
+  '//lib:gwtorm',
+  '//lib/guice:guice',
+  '//lib/guice:guice-assistedinject',
+  '//lib/guice:guice-servlet',
+  '//lib/jgit:jgit',
+  '//lib/log:api',
+]
+
 java_library(
   name = 'gpg',
   srcs = glob(['src/main/java/**/*.java']),
-  deps = [
-    '//gerrit-common:server',
-    '//gerrit-extension-api:api',
-    '//gerrit-reviewdb:server',
-    '//gerrit-server:server',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib/guice:guice',
-    '//lib/guice:guice-assistedinject',
-    '//lib/guice:guice-servlet',
-    '//lib/jgit:jgit',
-    '//lib/log:api',
-  ],
-  provided_deps = [
-    '//lib/bouncycastle:bcprov',
+  provided_deps = DEPS + [
     '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcprov',
   ],
   visibility = ['PUBLIC'],
 )
 
-TESTUTIL_SRCS = [
-  'src/test/java/com/google/gerrit/gpg/testutil/TestKey.java',
-]
+TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java'])
 
 java_library(
   name = 'testutil',
   srcs = TESTUTIL_SRCS,
-  deps = [
+  deps = DEPS + [
     ':gpg',
-    '//lib:guava',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcprov',
-    '//lib/jgit:jgit',
   ],
   visibility = ['PUBLIC'],
 )
@@ -44,20 +41,15 @@
     ['src/test/java/**/*.java'],
     excludes = TESTUTIL_SRCS,
   ),
-  deps = [
+  deps = DEPS + [
     ':gpg',
     ':testutil',
-    '//gerrit-extension-api:api',
-    '//gerrit-reviewdb:server',
-    '//gerrit-server:server',
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-lucene:lucene',  
     '//gerrit-server:testutil',
-    '//lib:guava',
-    '//lib:gwtorm',
     '//lib:truth',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcprov',
-    '//lib/guice:guice',
-    '//lib/jgit:jgit',
     '//lib/jgit:junit',
   ],
   source_under_test = [':gpg'],
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/CheckResult.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/CheckResult.java
index c41ecbe..74184bd 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/CheckResult.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/CheckResult.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.gpg;
 
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -21,22 +23,60 @@
 
 /** Result of checking an object like a key or signature. */
 public class CheckResult {
+  static CheckResult ok(String... problems) {
+    return create(Status.OK, problems);
+  }
+
+  static CheckResult bad(String... problems) {
+    return create(Status.BAD, problems);
+  }
+
+  static CheckResult trusted() {
+    return new CheckResult(Status.TRUSTED, Collections.<String> emptyList());
+  }
+
+  static CheckResult create(Status status, String... problems) {
+    List<String> problemList = problems.length > 0
+        ? Collections.unmodifiableList(Arrays.asList(problems))
+        : Collections.<String> emptyList();
+    return new CheckResult(status, problemList);
+  }
+
+  static CheckResult create(Status status, List<String> problems) {
+    return new CheckResult(status,
+        Collections.unmodifiableList(new ArrayList<>(problems)));
+  }
+
+  static CheckResult create(List<String> problems) {
+    return new CheckResult(
+        problems.isEmpty() ? Status.OK : Status.BAD,
+        Collections.unmodifiableList(problems));
+  }
+
+  private final Status status;
   private final List<String> problems;
 
-  CheckResult(String... problems) {
-    this(Arrays.asList(problems));
+  private CheckResult(Status status, List<String> problems) {
+    if (status == null) {
+      throw new IllegalArgumentException("status must not be null");
+    }
+    this.status = status;
+    this.problems = problems;
   }
 
-  CheckResult(List<String> problems) {
-    this.problems = Collections.unmodifiableList(new ArrayList<>(problems));
-  }
-
-  /**
-   * @return whether the result is entirely ok, i.e. has passed any verification
-   *     or validation checks.
-   */
+  /** @return whether the result has status {@link Status#OK} or better. */
   public boolean isOk() {
-    return problems.isEmpty();
+    return status.compareTo(Status.OK) >= 0;
+  }
+
+  /** @return whether the result has status {@link Status#TRUSTED} or better. */
+  public boolean isTrusted() {
+    return status.compareTo(Status.TRUSTED) >= 0;
+  }
+
+  /** @return the status enum value associated with the object. */
+  public Status getStatus() {
+    return status;
   }
 
   /** @return any problems encountered during checking. */
@@ -47,12 +87,9 @@
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder(getClass().getSimpleName())
-        .append('[');
+        .append('[').append(status);
     for (int i = 0; i < problems.size(); i++) {
-      if (i > 0) {
-        sb.append(", ");
-      }
-      sb.append(problems.get(i));
+      sb.append(i == 0 ? ": " : ", ").append(problems.get(i));
     }
     return sb.append(']').toString();
   }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
index 6fd8bac..fa78f01 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
@@ -19,6 +19,9 @@
 import org.eclipse.jgit.util.NB;
 
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 public class Fingerprint {
   private final byte[] fp;
@@ -33,6 +36,18 @@
         NB.decodeUInt16(fp, 16), NB.decodeUInt16(fp, 18));
   }
 
+  public static long getId(byte[] fp) {
+    return NB.decodeInt64(fp, 12);
+  }
+
+  public static Map<Long, Fingerprint> byId(Iterable<Fingerprint> fps) {
+    Map<Long, Fingerprint> result = new HashMap<>();
+    for (Fingerprint fp : fps) {
+      result.put(fp.getId(), fp);
+    }
+    return Collections.unmodifiableMap(result);
+  }
+
   private static byte[] checkLength(byte[] fp) {
     checkArgument(fp.length == 20,
         "fingerprint must be 20 bytes, got %s", fp.length);
@@ -54,6 +69,23 @@
     this.fp = checkLength(fp);
   }
 
+  /**
+   * Wrap a portion of a fingerprint byte array.
+   * <p>
+   * Unlike {@link #Fingerprint(byte[])}, creates a new copy of the byte array.
+   *
+   * @param buf byte array to wrap; must have at least {@code off + 20} bytes.
+   * @param off offset in buf.
+   */
+  public Fingerprint(byte[] buf, int off) {
+    int expected = 20 + off;
+    checkArgument(buf.length >= expected,
+        "fingerprint buffer must have at least %s bytes, got %s",
+        expected, buf.length);
+    this.fp = new byte[20];
+    System.arraycopy(buf, off, fp, 0, 20);
+  }
+
   public byte[] get() {
     return fp;
   }
@@ -79,6 +111,6 @@
   }
 
   public long getId() {
-    return NB.decodeInt64(fp, 12);
+    return getId(fp);
   }
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 52cddf2..c3c886f 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -17,13 +17,20 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
+import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -32,6 +39,7 @@
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.PushCertificateIdent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -39,7 +47,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -48,53 +56,148 @@
  * For Gerrit, keys must contain a self-signed user ID certification matching a
  * trusted external ID in the database, or an email address thereof.
  */
-@Singleton
 public class GerritPublicKeyChecker extends PublicKeyChecker {
   private static final Logger log =
       LoggerFactory.getLogger(GerritPublicKeyChecker.class);
 
-  private final String webUrl;
-  private final Provider<IdentifiedUser> userProvider;
+  @Singleton
+  public static class Factory {
+    private final Provider<ReviewDb> db;
+    private final String webUrl;
+    private final IdentifiedUser.GenericFactory userFactory;
+    private final int maxTrustDepth;
+    private final ImmutableMap<Long, Fingerprint> trusted;
 
-  @Inject
-  GerritPublicKeyChecker(
-      @CanonicalWebUrl String webUrl,
-      Provider<IdentifiedUser> userProvider) {
-    this.webUrl = webUrl;
-    this.userProvider = userProvider;
+    @Inject
+    Factory(@GerritServerConfig Config cfg,
+        Provider<ReviewDb> db,
+        IdentifiedUser.GenericFactory userFactory,
+        @CanonicalWebUrl String webUrl) {
+      this.db = db;
+      this.webUrl = webUrl;
+      this.userFactory = userFactory;
+      this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0);
+
+      String[] strs = cfg.getStringList("receive", null, "trustedKey");
+      if (strs.length != 0) {
+        Map<Long, Fingerprint> fps =
+            Maps.newHashMapWithExpectedSize(strs.length);
+        for (String str : strs) {
+          str = CharMatcher.WHITESPACE.removeFrom(str).toUpperCase();
+          Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
+          fps.put(fp.getId(), fp);
+        }
+        trusted = ImmutableMap.copyOf(fps);
+      } else {
+        trusted = null;
+      }
+    }
+
+    public GerritPublicKeyChecker create() {
+      return new GerritPublicKeyChecker(this);
+    }
+
+    public GerritPublicKeyChecker create(IdentifiedUser expectedUser,
+        PublicKeyStore store) {
+      GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this);
+      checker.setExpectedUser(expectedUser);
+      checker.setStore(store);
+      return checker;
+    }
+  }
+
+  private final Provider<ReviewDb> db;
+  private final String webUrl;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  private IdentifiedUser expectedUser;
+
+  private GerritPublicKeyChecker(Factory factory) {
+    this.db = factory.db;
+    this.webUrl = factory.webUrl;
+    this.userFactory = factory.userFactory;
+    if (factory.trusted != null) {
+      enableTrust(factory.maxTrustDepth, factory.trusted);
+    }
+  }
+
+   /**
+    * Set the expected user for this checker.
+    * <p>
+    * If set, the top-level key passed to {@link #check(PGPPublicKey)} must
+    * belong to the given user. (Other keys checked in the course of verifying
+    * the web of trust are checked against the set of identities in the database
+    * belonging to the same user as the key.)
+    */
+  public GerritPublicKeyChecker setExpectedUser(IdentifiedUser expectedUser) {
+    this.expectedUser = expectedUser;
+    return this;
   }
 
   @Override
-  public void checkCustom(PGPPublicKey key, long expectedKeyId,
-      List<String> problems) {
+  public CheckResult checkCustom(PGPPublicKey key, int depth) {
     try {
-      Set<String> allowedUserIds = getAllowedUserIds();
-      if (allowedUserIds.isEmpty()) {
-        problems.add("No identities found for user; check "
-            + webUrl + "#" + PageLinks.SETTINGS_WEBIDENT);
-        return;
+      if (depth == 0 && expectedUser != null) {
+        return checkIdsForExpectedUser(key);
+      } else {
+        return checkIdsForArbitraryUser(key);
       }
+    } catch (PGPException | OrmException e) {
+      String msg = "Error checking user IDs for key";
+      log.warn(msg + " " + keyIdToString(key.getKeyID()), e);
+      return CheckResult.bad(msg);
+    }
+  }
 
-      @SuppressWarnings("unchecked")
-      Iterator<String> userIds = key.getUserIDs();
-      while (userIds.hasNext()) {
-        String userId = userIds.next();
-        if (isAllowed(userId, allowedUserIds)) {
-          Iterator<PGPSignature> sigs = getSignaturesForId(key, userId);
-          while (sigs.hasNext()) {
-            if (isValidCertification(key, sigs.next(), userId)) {
-              return;
-            }
+  private CheckResult checkIdsForExpectedUser(PGPPublicKey key)
+      throws PGPException {
+    Set<String> allowedUserIds = getAllowedUserIds(expectedUser);
+    if (allowedUserIds.isEmpty()) {
+      return CheckResult.bad("No identities found for user; check "
+          + webUrl + "#" + PageLinks.SETTINGS_WEBIDENT);
+    }
+    if (hasAllowedUserId(key, allowedUserIds)) {
+      return CheckResult.trusted();
+    }
+    return CheckResult.bad(missingUserIds(allowedUserIds));
+  }
+
+  private CheckResult checkIdsForArbitraryUser(PGPPublicKey key)
+      throws PGPException, OrmException {
+    AccountExternalId extId = db.get().accountExternalIds().get(
+        toExtIdKey(key));
+    if (extId == null) {
+      return CheckResult.bad("Key is not associated with any users");
+    }
+    IdentifiedUser user = userFactory.create(db, extId.getAccountId());
+    Set<String> allowedUserIds = getAllowedUserIds(user);
+    if (allowedUserIds.isEmpty()) {
+      return CheckResult.bad("No identities found for user");
+    }
+    if (hasAllowedUserId(key, allowedUserIds)) {
+      return CheckResult.trusted();
+    }
+    return CheckResult.bad(
+        "Key does not contain any valid certifications for user's identities");
+  }
+
+  private boolean hasAllowedUserId(PGPPublicKey key, Set<String> allowedUserIds)
+      throws PGPException {
+    @SuppressWarnings("unchecked")
+    Iterator<String> userIds = key.getUserIDs();
+    while (userIds.hasNext()) {
+      String userId = userIds.next();
+      if (isAllowed(userId, allowedUserIds)) {
+        Iterator<PGPSignature> sigs = getSignaturesForId(key, userId);
+        while (sigs.hasNext()) {
+          if (isValidCertification(key, sigs.next(), userId)) {
+            return true;
           }
         }
       }
-
-      problems.add(missingUserIds(allowedUserIds));
-    } catch (PGPException e) {
-      String msg = "Error checking user IDs for key";
-      log.warn(msg + " " + keyIdToString(key.getKeyID()), e);
-      problems.add(msg);
     }
+
+    return false;
   }
 
   @SuppressWarnings("unchecked")
@@ -105,8 +208,7 @@
         Collections.emptyIterator());
   }
 
-  private Set<String> getAllowedUserIds() {
-    IdentifiedUser user = userProvider.get();
+  private Set<String> getAllowedUserIds(IdentifiedUser user) {
     Set<String> result = new HashSet<>();
     result.addAll(user.getEmailAddresses());
     for (AccountExternalId extId : user.state().getExternalIds()) {
@@ -156,4 +258,10 @@
     }
     return sb.toString();
   }
+
+  static AccountExternalId.Key toExtIdKey(PGPPublicKey key) {
+    return new AccountExternalId.Key(
+        SCHEME_GPGKEY,
+        BaseEncoding.base16().encode(key.getFingerprint()));
+  }
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
new file mode 100644
index 0000000..30983ac
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.gpg;
+
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class GerritPushCertificateChecker extends PushCertificateChecker {
+  public interface Factory {
+    GerritPushCertificateChecker create(IdentifiedUser expectedUser);
+  }
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+
+  @AssistedInject
+  GerritPushCertificateChecker(
+      GerritPublicKeyChecker.Factory keyCheckerFactory,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers,
+      @Assisted IdentifiedUser expectedUser) {
+    super(keyCheckerFactory.create().setExpectedUser(expectedUser));
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
+  }
+
+  @Override
+  protected Repository getRepository() throws IOException {
+    return repoManager.openRepository(allUsers);
+  }
+
+  @Override
+  protected boolean shouldClose(Repository repo) {
+    return true;
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
index fc1953a..bbf61b8 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
@@ -14,32 +14,15 @@
 
 package com.google.gerrit.gpg;
 
-import static com.google.gerrit.gpg.server.GpgKey.GPG_KEY_KIND;
-import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
-
-import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
-import com.google.gerrit.extensions.common.GpgKeyInfo;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
-import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.gpg.api.GpgApiAdapterImpl;
-import com.google.gerrit.gpg.api.GpgKeyApiImpl;
-import com.google.gerrit.gpg.server.DeleteGpgKey;
-import com.google.gerrit.gpg.server.GpgKeys;
-import com.google.gerrit.gpg.server.PostGpgKeys;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.gpg.api.GpgApiModule;
 import com.google.gerrit.server.EnableSignedPush;
-import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.api.accounts.GpgApiAdapter;
 
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.List;
-import java.util.Map;
-
-public class GpgModule extends RestApiModule {
+public class GpgModule extends FactoryModule {
   private static final Logger log = LoggerFactory.getLogger(GpgModule.class);
 
   private final Config cfg;
@@ -52,6 +35,8 @@
   protected void configure() {
     boolean configEnableSignedPush =
         cfg.getBoolean("receive", null, "enableSignedPush", false);
+    boolean configEditGpgKeys =
+        cfg.getBoolean("gerrit", null, "editGpgKeys", true);
     boolean havePgp = BouncyCastleUtil.havePGP();
     boolean enableSignedPush = configEnableSignedPush && havePgp;
     bindConstant().annotatedWith(EnableSignedPush.class).to(enableSignedPush);
@@ -60,40 +45,10 @@
       log.info("Bouncy Castle PGP not installed; signed push verification is"
           + " disabled");
     }
-    if (!enableSignedPush) {
-      bind(GpgApiAdapter.class).to(NoGpgApi.class);
-      return;
+    if (enableSignedPush) {
+      install(new SignedPushModule());
+      factory(GerritPushCertificateChecker.Factory.class);
     }
-
-    install(new SignedPushModule());
-    bind(GpgApiAdapter.class).to(GpgApiAdapterImpl.class);
-    factory(GpgKeyApiImpl.Factory.class);
-
-    DynamicMap.mapOf(binder(), GPG_KEY_KIND);
-
-    child(ACCOUNT_KIND, "gpgkeys").to(GpgKeys.class);
-    post(ACCOUNT_KIND, "gpgkeys").to(PostGpgKeys.class);
-    get(GPG_KEY_KIND).to(GpgKeys.Get.class);
-    delete(GPG_KEY_KIND).to(DeleteGpgKey.class);
-  }
-
-  private static class NoGpgApi implements GpgApiAdapter {
-    private static final String MSG = "GPG key APIs disabled";
-
-    @Override
-    public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account) {
-      throw new NotImplementedException(MSG);
-    }
-
-    @Override
-    public Map<String, GpgKeyInfo> putGpgKeys(AccountResource account,
-        List<String> add, List<String> delete) {
-      throw new NotImplementedException(MSG);
-    }
-
-    @Override
-    public GpgKeyApi gpgKey(AccountResource account, IdString idStr) {
-      throw new NotImplementedException(MSG);
-    }
+    install(new GpgApiModule(enableSignedPush && configEditGpgKeys));
   }
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 7b7aabb..e4c81df 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -14,65 +14,476 @@
 
 package com.google.gerrit.gpg;
 
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.OK;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED;
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_COMPROMISED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_RETIRED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_SUPERSEDED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.NO_REASON;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
 
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+
+import org.bouncycastle.bcpg.SignatureSubpacket;
+import org.bouncycastle.bcpg.SignatureSubpacketTags;
+import org.bouncycastle.bcpg.sig.RevocationKey;
+import org.bouncycastle.bcpg.sig.RevocationReason;
+import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /** Checker for GPG public keys for use in a push certificate. */
 public class PublicKeyChecker {
+  private static final Logger log =
+      LoggerFactory.getLogger(PublicKeyChecker.class);
+
+  // https://tools.ietf.org/html/rfc4880#section-5.2.3.13
+  private static final int COMPLETE_TRUST = 120;
+
+  private PublicKeyStore store;
+  private Map<Long, Fingerprint> trusted;
+  private int maxTrustDepth;
+  private Date effectiveTime = new Date();
+
   /**
-   * Check a public key.
+   * Enable web-of-trust checks.
+   * <p>
+   * If enabled, a store must be set with {@link #setStore(PublicKeyStore)}.
+   * (These methods are separate since the store is a closeable resource that
+   * may not be available when reading trusted keys from a config.)
    *
-   * @param key the public key.
+   * @param maxTrustDepth maximum depth to search while looking for a trusted
+   *     key.
+   * @param trusted ultimately trusted key fingerprints, keyed by fingerprint;
+   *     may not be empty. To construct a map, see {@link
+   *     Fingerprint#byId(Iterable)}.
+   * @return a reference to this object.
    */
-  public final CheckResult check(PGPPublicKey key) {
-    return check(key, key.getKeyID());
+  public PublicKeyChecker enableTrust(int maxTrustDepth,
+      Map<Long, Fingerprint> trusted) {
+    if (maxTrustDepth <= 0) {
+      throw new IllegalArgumentException(
+          "maxTrustDepth must be positive, got: " + maxTrustDepth);
+    }
+    if (trusted == null || trusted.isEmpty()) {
+        throw new IllegalArgumentException(
+            "at least one trusted key is required");
+    }
+    this.maxTrustDepth = maxTrustDepth;
+    this.trusted = trusted;
+    return this;
+  }
+
+  /** Disable web-of-trust checks. */
+  public PublicKeyChecker disableTrust() {
+    trusted = null;
+    return this;
+  }
+
+  /** Set the public key store for reading keys referenced in signatures. */
+  public PublicKeyChecker setStore(PublicKeyStore store) {
+    if (store == null) {
+      throw new IllegalArgumentException("PublicKeyStore is required");
+    }
+    this.store = store;
+    return this;
+  }
+
+  /**
+   * Set the effective time for checking the key.
+   * <p>
+   * If set, check whether the key should be considered valid (e.g. unexpired)
+   * as of this time.
+   *
+   * @param effectiveTime effective time.
+   * @return a reference to this object.
+   */
+  public PublicKeyChecker setEffectiveTime(Date effectiveTime) {
+    this.effectiveTime = effectiveTime;
+    return this;
+  }
+
+  protected Date getEffectiveTime() {
+    return effectiveTime;
   }
 
   /**
    * Check a public key.
    *
    * @param key the public key.
-   * @param expectedKeyId the key ID that the caller expects.
+   * @return the result of the check.
    */
-  public final CheckResult check(PGPPublicKey key, long expectedKeyId) {
-    List<String> problems = new ArrayList<>();
-    if (key.getKeyID() != expectedKeyId) {
-      problems.add(
-          "Public key does not match ID " + keyIdToString(expectedKeyId));
+  public final CheckResult check(PGPPublicKey key) {
+    if (store == null) {
+      throw new IllegalStateException("PublicKeyStore is required");
     }
-    if (key.isRevoked()) {
-      // TODO(dborowitz): isRevoked is overeager:
-      // http://www.bouncycastle.org/jira/browse/BJB-45
-      problems.add("Key is revoked");
-    }
-
-    long validSecs = key.getValidSeconds();
-    if (validSecs != 0) {
-      long createdSecs = key.getCreationTime().getTime() / 1000;
-      long nowSecs = System.currentTimeMillis() / 1000;
-      if (nowSecs - createdSecs > validSecs) {
-        problems.add("Key is expired");
-      }
-    }
-    checkCustom(key, expectedKeyId, problems);
-    return new CheckResult(problems);
+    return check(key, 0, true,
+        trusted != null ? new HashSet<Fingerprint>() : null);
   }
 
   /**
    * Perform custom checks.
    * <p>
-   * Default implementation does nothing, but may be overridden by subclasses.
+   * Default implementation reports no problems, but may be overridden by
+   * subclasses.
    *
    * @param key the public key.
-   * @param expectedKeyId the key ID that the caller expects.
-   * @param problems list to which any problems should be added.
+   * @param depth the depth from the initial key passed to {@link #check(
+   *     PGPPublicKey)}: 0 if this was the initial key, up to a maximum of
+   *     {@code maxTrustDepth}.
+   * @return the result of the custom check.
    */
-  public void checkCustom(PGPPublicKey key, long expectedKeyId,
+  public CheckResult checkCustom(PGPPublicKey key, int depth) {
+    return CheckResult.ok();
+  }
+
+  private CheckResult check(PGPPublicKey key, int depth, boolean expand,
+      Set<Fingerprint> seen) {
+    CheckResult basicResult = checkBasic(key, effectiveTime);
+    CheckResult customResult = checkCustom(key, depth);
+    CheckResult trustResult = checkWebOfTrust(key, store, depth, seen);
+    if (!expand && !trustResult.isTrusted()) {
+      trustResult = CheckResult.create(trustResult.getStatus(),
+          "Key is not trusted");
+    }
+
+    List<String> problems = new ArrayList<>(
+        basicResult.getProblems().size()
+        + customResult.getProblems().size()
+        + trustResult.getProblems().size());
+    problems.addAll(basicResult.getProblems());
+    problems.addAll(customResult.getProblems());
+    problems.addAll(trustResult.getProblems());
+
+    Status status;
+    if (basicResult.getStatus() == BAD
+        || customResult.getStatus() == BAD
+        || trustResult.getStatus() == BAD) {
+      // Any BAD result and the final result is BAD.
+      status = BAD;
+    } else if (trustResult.getStatus() == TRUSTED) {
+      // basicResult is BAD or OK, whereas trustResult is BAD or TRUSTED. If
+      // TRUSTED, we trust the final result.
+      status = TRUSTED;
+    } else {
+      // All results were OK or better, but trustResult was not TRUSTED. Don't
+      // let subclasses bypass checkWebOfTrust by returning TRUSTED; just return
+      // OK here.
+      status = OK;
+    }
+    return CheckResult.create(status, problems);
+  }
+
+  private CheckResult checkBasic(PGPPublicKey key, Date now) {
+    List<String> problems = new ArrayList<>(2);
+    gatherRevocationProblems(key, now, problems);
+
+    long validMs = key.getValidSeconds() * 1000;
+    if (validMs != 0) {
+      long msSinceCreation = now.getTime() - key.getCreationTime().getTime();
+      if (msSinceCreation > validMs) {
+        problems.add("Key is expired");
+      }
+    }
+    return CheckResult.create(problems);
+  }
+
+  private void gatherRevocationProblems(PGPPublicKey key, Date now,
       List<String> problems) {
-    // Default implementation does nothing.
+    try {
+      List<PGPSignature> revocations = new ArrayList<>();
+      Map<Long, RevocationKey> revokers = new HashMap<>();
+      PGPSignature selfRevocation =
+          scanRevocations(key, now, revocations, revokers);
+      if (selfRevocation != null) {
+        RevocationReason reason = getRevocationReason(selfRevocation);
+        if (isRevocationValid(selfRevocation, reason, now)) {
+          problems.add(reasonToString(reason));
+        }
+      } else {
+        checkRevocations(key, revocations, revokers, problems);
+      }
+    } catch (PGPException | IOException e) {
+      problems.add("Error checking key revocation");
+    }
+  }
+
+  private static boolean isRevocationValid(PGPSignature revocation,
+      RevocationReason reason, Date now) {
+    // RFC4880 states:
+    // "If a key has been revoked because of a compromise, all signatures
+    // created by that key are suspect. However, if it was merely superseded or
+    // retired, old signatures are still valid."
+    //
+    // Note that GnuPG does not implement this correctly, as it does not
+    // consider the revocation reason and timestamp when checking whether a
+    // signature (data or certification) is valid.
+    return reason.getRevocationReason() == KEY_COMPROMISED
+        || revocation.getCreationTime().before(now);
+  }
+
+  private PGPSignature scanRevocations(PGPPublicKey key, Date now,
+      List<PGPSignature> revocations, Map<Long, RevocationKey> revokers)
+      throws PGPException {
+    @SuppressWarnings("unchecked")
+    Iterator<PGPSignature> allSigs = key.getSignatures();
+    while (allSigs.hasNext()) {
+      PGPSignature sig = allSigs.next();
+      switch (sig.getSignatureType()) {
+        case KEY_REVOCATION:
+          if (sig.getKeyID() == key.getKeyID()) {
+            sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+            if (sig.verifyCertification(key)) {
+              return sig;
+            }
+          } else {
+            RevocationReason reason = getRevocationReason(sig);
+            if (reason != null && isRevocationValid(sig, reason, now)) {
+              revocations.add(sig);
+            }
+          }
+          break;
+        case DIRECT_KEY:
+          RevocationKey r = getRevocationKey(key, sig);
+          if (r != null) {
+            revokers.put(Fingerprint.getId(r.getFingerprint()), r);
+          }
+          break;
+      }
+    }
+    return null;
+  }
+
+  private RevocationKey getRevocationKey(PGPPublicKey key, PGPSignature sig)
+      throws PGPException {
+    if (sig.getKeyID() != key.getKeyID()) {
+      return null;
+    }
+    SignatureSubpacket sub =
+        sig.getHashedSubPackets().getSubpacket(REVOCATION_KEY);
+    if (sub == null) {
+      return null;
+    }
+    sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+    if (!sig.verifyCertification(key)) {
+      return null;
+    }
+
+    return new RevocationKey(sub.isCritical(), sub.getData());
+  }
+
+  private void checkRevocations(PGPPublicKey key,
+      List<PGPSignature> revocations, Map<Long, RevocationKey> revokers,
+      List<String> problems)
+      throws PGPException, IOException {
+    for (PGPSignature revocation : revocations) {
+      RevocationKey revoker = revokers.get(revocation.getKeyID());
+      if (revoker == null) {
+        continue; // Not a designated revoker.
+      }
+      byte[] rfp = revoker.getFingerprint();
+      PGPPublicKeyRing revokerKeyRing = store.get(rfp);
+      if (revokerKeyRing == null) {
+        // Revoker is authorized and there is a revocation signature by this
+        // revoker, but the key is not in the store so we can't verify the
+        // signature.
+        log.info("Key " + Fingerprint.toString(key.getFingerprint())
+            + " is revoked by " + Fingerprint.toString(rfp)
+            + ", which is not in the store. Assuming revocation is valid.");
+        problems.add(reasonToString(getRevocationReason(revocation)));
+        continue;
+      }
+      PGPPublicKey rk = revokerKeyRing.getPublicKey();
+      if (rk.getAlgorithm() != revoker.getAlgorithm()) {
+        continue;
+      }
+      if (!checkBasic(rk, revocation.getCreationTime()).isOk()) {
+        // Revoker's key was expired or revoked at time of revocation, so the
+        // revocation is invalid.
+        continue;
+      }
+      revocation.init(new BcPGPContentVerifierBuilderProvider(), rk);
+      if (revocation.verifyCertification(key)) {
+        problems.add(reasonToString(getRevocationReason(revocation)));
+      }
+    }
+  }
+
+  private static RevocationReason getRevocationReason(PGPSignature sig) {
+    if (sig.getSignatureType() != KEY_REVOCATION) {
+      throw new IllegalArgumentException(
+          "Expected KEY_REVOCATION signature, got " + sig.getSignatureType());
+    }
+    SignatureSubpacket sub =
+        sig.getHashedSubPackets().getSubpacket(REVOCATION_REASON);
+    if (sub == null) {
+      return null;
+    }
+    return new RevocationReason(sub.isCritical(), sub.getData());
+  }
+
+  private static String reasonToString(RevocationReason reason) {
+    StringBuilder r = new StringBuilder("Key is revoked (");
+    if (reason == null) {
+      return r.append("no reason provided)").toString();
+    }
+    switch (reason.getRevocationReason()) {
+      case NO_REASON:
+        r.append("no reason code specified");
+        break;
+      case KEY_SUPERSEDED:
+        r.append("superseded");
+        break;
+      case KEY_COMPROMISED:
+        r.append("key material has been compromised");
+        break;
+      case KEY_RETIRED:
+        r.append("retired and no longer valid");
+        break;
+      default:
+        r.append("reason code ")
+            .append(Integer.toString(reason.getRevocationReason()))
+            .append(')');
+        break;
+    }
+    r.append(')');
+    String desc = reason.getRevocationDescription();
+    if (!desc.isEmpty()) {
+      r.append(": ").append(desc);
+    }
+    return r.toString();
+  }
+
+  private CheckResult checkWebOfTrust(PGPPublicKey key, PublicKeyStore store,
+      int depth, Set<Fingerprint> seen) {
+    if (trusted == null) {
+      // Trust checking not configured, server trusts all OK keys.
+      return CheckResult.trusted();
+    }
+    Fingerprint fp = new Fingerprint(key.getFingerprint());
+    if (seen.contains(fp)) {
+      return CheckResult.ok("Key is trusted in a cycle");
+    }
+    seen.add(fp);
+
+    Fingerprint trustedFp = trusted.get(key.getKeyID());
+    if (trustedFp != null && trustedFp.equals(fp)) {
+      return CheckResult.trusted(); // Directly trusted.
+    } else if (depth >= maxTrustDepth) {
+      return CheckResult.ok(
+          "No path of depth <= " + maxTrustDepth + " to a trusted key");
+    }
+
+    List<CheckResult> signerResults = new ArrayList<>();
+    @SuppressWarnings("unchecked")
+    Iterator<String> userIds = key.getUserIDs();
+    while (userIds.hasNext()) {
+      String userId = userIds.next();
+
+      // Don't check the timestamp of these certifications. This allows admins
+      // to correct untrusted keys by signing them with a trusted key, such that
+      // older signatures created by those keys retroactively appear valid.
+      @SuppressWarnings("unchecked")
+      Iterator<PGPSignature> sigs = key.getSignaturesForID(userId);
+
+      while (sigs.hasNext()) {
+        PGPSignature sig = sigs.next();
+        // TODO(dborowitz): Handle CERTIFICATION_REVOCATION.
+        if (sig.getSignatureType() != PGPSignature.DEFAULT_CERTIFICATION
+            && sig.getSignatureType() != PGPSignature.POSITIVE_CERTIFICATION) {
+          continue; // Not a certification.
+        }
+
+        PGPPublicKey signer = getSigner(store, sig, userId, key, signerResults);
+        // TODO(dborowitz): Require self certification.
+        if (signer == null
+            || Arrays.equals(signer.getFingerprint(), key.getFingerprint())) {
+          continue;
+        }
+        String subpacketProblem = checkTrustSubpacket(sig, depth);
+        if (subpacketProblem == null) {
+          CheckResult signerResult = check(signer, depth + 1, false, seen);
+          if (signerResult.isTrusted()) {
+            return CheckResult.trusted();
+          }
+        }
+        signerResults.add(CheckResult.ok(
+            "Certification by " + keyToString(signer)
+            + " is valid, but key is not trusted"));
+      }
+    }
+
+    List<String> problems = new ArrayList<>();
+    problems.add("No path to a trusted key");
+    for (CheckResult signerResult : signerResults) {
+      problems.addAll(signerResult.getProblems());
+    }
+    return CheckResult.create(OK, problems);
+  }
+
+  private static PGPPublicKey getSigner(PublicKeyStore store, PGPSignature sig,
+      String userId, PGPPublicKey key, List<CheckResult> results) {
+    try {
+      PGPPublicKeyRingCollection signers = store.get(sig.getKeyID());
+      if (!signers.getKeyRings().hasNext()) {
+        results.add(CheckResult.ok(
+            "Key " + keyIdToString(sig.getKeyID())
+            + " used for certification is not in store"));
+        return null;
+      }
+      PGPPublicKey signer = PublicKeyStore.getSigner(signers, sig, userId, key);
+      if (signer == null) {
+        results.add(CheckResult.ok(
+            "Certification by " + keyIdToString(sig.getKeyID())
+            + " is not valid"));
+        return null;
+      }
+      return signer;
+    } catch (PGPException | IOException e) {
+      results.add(CheckResult.ok(
+          "Error checking certification by " + keyIdToString(sig.getKeyID())));
+      return null;
+    }
+  }
+
+  private String checkTrustSubpacket(PGPSignature sig, int depth) {
+    SignatureSubpacket trustSub = sig.getHashedSubPackets().getSubpacket(
+        SignatureSubpacketTags.TRUST_SIG);
+    if (trustSub == null || trustSub.getData().length != 2) {
+      return "Certification is missing trust information";
+    }
+    byte amount = trustSub.getData()[1];
+    if (amount < COMPLETE_TRUST) {
+      return "Certification does not fully trust key";
+    }
+    byte level = trustSub.getData()[0];
+    int required = depth + 1;
+    if (level < required) {
+      return "Certification trusts to depth " + level
+          + ", but depth " + required + " is required";
+    }
+    return null;
   }
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
index a36052e..3d939a1 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -23,7 +23,9 @@
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -74,6 +76,52 @@
   /** Ref where GPG public keys are stored. */
   public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
 
+  /**
+   * Choose the public key that produced a signature.
+   * <p>
+   * @param keyRings candidate keys.
+   * @param sig signature object.
+   * @param data signed payload.
+   * @return the key chosen from {@code keyRings} that was able to verify the
+   *     signature, or {@code null} if none was found.
+   * @throws PGPException if an error occurred verifying the signature.
+   */
+  public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
+      PGPSignature sig, byte[] data) throws PGPException {
+    for (PGPPublicKeyRing kr : keyRings) {
+      PGPPublicKey k = kr.getPublicKey();
+      sig.init(new BcPGPContentVerifierBuilderProvider(), k);
+      sig.update(data);
+      if (sig.verify()) {
+        return k;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Choose the public key that produced a certification.
+   * <p>
+   * @param keyRings candidate keys.
+   * @param sig signature object.
+   * @param userId user ID being certified.
+   * @param key key being certified.
+   * @return the key chosen from {@code keyRings} that was able to verify the
+   *     certification, or {@code null} if none was found.
+   * @throws PGPException if an error occurred verifying the certification.
+   */
+  public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
+      PGPSignature sig, String userId, PGPPublicKey key) throws PGPException {
+    for (PGPPublicKeyRing kr : keyRings) {
+      PGPPublicKey k = kr.getPublicKey();
+      sig.init(new BcPGPContentVerifierBuilderProvider(), k);
+      if (sig.verifyCertification(userId, key)) {
+        return k;
+      }
+    }
+    return null;
+  }
+
   private final Repository repo;
   private ObjectReader reader;
   private RevCommit tip;
@@ -130,15 +178,39 @@
    */
   public PGPPublicKeyRingCollection get(long keyId)
       throws PGPException, IOException {
+    return new PGPPublicKeyRingCollection(get(keyId, null));
+  }
+
+  /**
+   * Read public key with the given fingerprint.
+   * <p>
+   * Keys should not be trusted unless checked with {@link PublicKeyChecker}.
+   * <p>
+   * Multiple calls to this method use the same state of the key ref; to reread
+   * the ref, call {@link #close()} first.
+   *
+   * @param fingerprint key fingerprint.
+   * @return the key if found, or {@code null}.
+   * @throws PGPException if an error occurred parsing the key data.
+   * @throws IOException if an error occurred reading the repository data.
+   */
+  public PGPPublicKeyRing get(byte[] fingerprint)
+      throws PGPException, IOException {
+    List<PGPPublicKeyRing> keyRings =
+        get(Fingerprint.getId(fingerprint), fingerprint);
+    return !keyRings.isEmpty() ? keyRings.get(0) : null;
+  }
+
+  private List<PGPPublicKeyRing> get(long keyId, byte[] fp) throws IOException {
     if (reader == null) {
       load();
     }
     if (notes == null) {
-      return empty();
+      return Collections.emptyList();
     }
     Note note = notes.getNote(keyObjectId(keyId));
     if (note == null) {
-      return empty();
+      return Collections.emptyList();
     }
 
     List<PGPPublicKeyRing> keys = new ArrayList<>();
@@ -152,12 +224,16 @@
         }
         Object obj = it.next();
         if (obj instanceof PGPPublicKeyRing) {
-          keys.add((PGPPublicKeyRing) obj);
+          PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
+          if (fp == null
+              || Arrays.equals(fp, kr.getPublicKey().getFingerprint())) {
+            keys.add(kr);
+          }
         }
         checkState(!it.hasNext(),
             "expected one PGP object per ArmoredInputStream");
       }
-      return new PGPPublicKeyRingCollection(keys);
+      return keys;
     }
   }
 
@@ -303,7 +379,7 @@
     }
     if (toWrite.size() == existing.size()) {
       return;
-    } else if (toWrite.size() > 0) {
+    } else if (!toWrite.isEmpty()) {
       notes.set(keyObjectId(keyId),
           ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
     } else {
@@ -327,12 +403,6 @@
     return out.toByteArray();
   }
 
-  private static PGPPublicKeyRingCollection empty()
-      throws PGPException, IOException {
-    return new PGPPublicKeyRingCollection(
-        Collections.<PGPPublicKeyRing> emptyList());
-  }
-
   public static String keyToString(PGPPublicKey key) {
     @SuppressWarnings("unchecked")
     Iterator<String> it = key.getUserIDs();
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 86a33ab..0a0fff7 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -14,19 +14,23 @@
 
 package com.google.gerrit.gpg;
 
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.OK;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED;
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 
+import com.google.common.base.Joiner;
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+
 import org.bouncycastle.bcpg.ArmoredInputStream;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPObjectFactory;
 import org.bouncycastle.openpgp.PGPPublicKey;
-import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.PGPSignatureList;
 import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
-import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PushCertificate;
@@ -44,10 +48,37 @@
   private static final Logger log =
       LoggerFactory.getLogger(PushCertificateChecker.class);
 
+  public static class Result {
+    private final PGPPublicKey key;
+    private final CheckResult checkResult;
+
+    private Result(PGPPublicKey key, CheckResult checkResult) {
+      this.key = key;
+      this.checkResult = checkResult;
+    }
+
+    public PGPPublicKey getPublicKey() {
+      return key;
+    }
+
+    public CheckResult getCheckResult() {
+      return checkResult;
+    }
+  }
+
   private final PublicKeyChecker publicKeyChecker;
 
+  private boolean checkNonce;
+
   protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
     this.publicKeyChecker = publicKeyChecker;
+    checkNonce = true;
+  }
+
+  /** Set whether to check the status of the nonce; defaults to true. */
+  public PushCertificateChecker setCheckNonce(boolean checkNonce) {
+    this.checkNonce = checkNonce;
+    return this;
   }
 
   /**
@@ -55,33 +86,65 @@
    *
    * @return result of the check.
    */
-  public final CheckResult check(PushCertificate cert) {
-    if (cert.getNonceStatus() != NonceStatus.OK) {
-      return new CheckResult("Invalid nonce");
+  public final Result check(PushCertificate cert) {
+    if (checkNonce && cert.getNonceStatus() != NonceStatus.OK) {
+      return new Result(null, CheckResult.bad("Invalid nonce"));
     }
-    List<String> problems = new ArrayList<>();
+    List<CheckResult> results = new ArrayList<>(2);
+    Result sigResult = null;
     try {
       PGPSignature sig = readSignature(cert);
       if (sig != null) {
         @SuppressWarnings("resource")
         Repository repo = getRepository();
         try (PublicKeyStore store = new PublicKeyStore(repo)) {
-          checkSignature(sig, cert, store.get(sig.getKeyID()), problems);
-          checkCustom(repo, problems);
+          sigResult = checkSignature(sig, cert, store);
+          results.add(checkCustom(repo));
         } finally {
           if (shouldClose(repo)) {
             repo.close();
           }
         }
       } else {
-        problems.add("Invalid signature format");
+        results.add(CheckResult.bad("Invalid signature format"));
       }
     } catch (PGPException | IOException e) {
       String msg = "Internal error checking push certificate";
       log.error(msg, e);
-      problems.add(msg);
+      results.add(CheckResult.bad(msg));
     }
-    return new CheckResult(problems);
+
+    return combine(sigResult, results);
+  }
+
+  private static Result combine(Result sigResult, List<CheckResult> results) {
+    // Combine results:
+    //  - If any input result is BAD, the final result is bad.
+    //  - If sigResult is TRUSTED and no other result is BAD, the final result
+    //    is TRUSTED.
+    //  - Otherwise, the result is OK.
+    List<String> problems = new ArrayList<>();
+    boolean bad = false;
+    for (CheckResult result : results) {
+      problems.addAll(result.getProblems());
+      bad |= result.getStatus() == BAD;
+    }
+    Status status = bad ? BAD : OK;
+
+    PGPPublicKey key;
+    if (sigResult != null) {
+      key = sigResult.getPublicKey();
+      CheckResult cr = sigResult.getCheckResult();
+      problems.addAll(cr.getProblems());
+      if (cr.getStatus() == BAD) {
+        status = BAD;
+      } else if (!bad && cr.getStatus() == TRUSTED) {
+        status = TRUSTED;
+      }
+    } else {
+      key = null;
+    }
+    return new Result(key, CheckResult.create(status, problems));
   }
 
   /**
@@ -104,13 +167,14 @@
   /**
    * Perform custom checks.
    * <p>
-   * Default implementation does nothing, but may be overridden by subclasses.
+   * Default implementation reports no problems, but may be overridden by
+   * subclasses.
    *
    * @param repo a repository previously returned by {@link #getRepository()}.
-   * @param problems list to which any problems should be added.
+   * @return the result of the custom check.
    */
-  protected void checkCustom(Repository repo, List<String> problems) {
-    // Default implementation does nothing.
+  protected CheckResult checkCustom(Repository repo) {
+    return CheckResult.ok();
   }
 
   private PGPSignature readSignature(PushCertificate cert) throws IOException {
@@ -129,47 +193,33 @@
     return null;
   }
 
-  private void checkSignature(PGPSignature sig,
-      PushCertificate cert, PGPPublicKeyRingCollection keys,
-      List<String> problems) {
-    List<String> deferredProblems = new ArrayList<>();
-    boolean anyKeys = false;
-    for (PGPPublicKeyRing kr : keys) {
-      PGPPublicKey k = kr.getPublicKey();
-      anyKeys = true;
-      try {
-        sig.init(new BcPGPContentVerifierBuilderProvider(), k);
-        sig.update(Constants.encode(cert.toText()));
-        if (!sig.verify()) {
-          // TODO(dborowitz): Privacy issues with exposing fingerprint/user ID
-          // of keys having the same ID as the pusher's key?
-          deferredProblems.add(
-              "Signature not valid with public key: " + keyToString(k));
-          continue;
-        }
-        CheckResult result = publicKeyChecker.check(k, sig.getKeyID());
-        if (result.isOk()) {
-          return;
-        }
-        StringBuilder err = new StringBuilder("Invalid public key ")
-            .append(keyToString(k))
-            .append(":");
-        for (int i = 0; i < result.getProblems().size(); i++) {
-          err.append('\n').append("  ").append(result.getProblems().get(i));
-        }
-        problems.add(err.toString());
-        return;
-      } catch (PGPException e) {
-        deferredProblems.add(
-            "Error checking signature with public key " + keyToString(k)
-            + ": " + e.getMessage());
-      }
+  private Result checkSignature(PGPSignature sig, PushCertificate cert,
+      PublicKeyStore store) throws PGPException, IOException {
+    PGPPublicKeyRingCollection keys = store.get(sig.getKeyID());
+    if (!keys.getKeyRings().hasNext()) {
+      return new Result(null,
+          CheckResult.bad("No public keys found for key ID "
+              + keyIdToString(sig.getKeyID())));
     }
-    if (!anyKeys) {
-      problems.add(
-          "No public keys found for key ID " + keyIdToString(sig.getKeyID()));
-    } else {
-      problems.addAll(deferredProblems);
+    PGPPublicKey signer =
+        PublicKeyStore.getSigner(keys, sig, Constants.encode(cert.toText()));
+    if (signer == null) {
+      return new Result(null,
+          CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID())
+              + " is not valid"));
     }
+    CheckResult result = publicKeyChecker
+        .setStore(store)
+        .setEffectiveTime(sig.getCreationTime())
+        .check(signer);
+    if (!result.getProblems().isEmpty()) {
+      StringBuilder err = new StringBuilder("Invalid public key ")
+          .append(keyToString(signer))
+          .append(":\n  ")
+          .append(Joiner.on("\n  ").join(result.getProblems()));
+      return new Result(
+          signer, CheckResult.create(result.getStatus(), err.toString()));
+    }
+    return new Result(signer, result);
   }
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
index 7508806..bc027cd 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.gpg;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.EnableSignedPush;
@@ -33,6 +32,7 @@
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PreReceiveHook;
 import org.eclipse.jgit.transport.PreReceiveHookChain;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.SignedPushConfig;
@@ -42,6 +42,8 @@
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Random;
 
 class SignedPushModule extends AbstractModule {
@@ -53,7 +55,6 @@
     if (!BouncyCastleUtil.havePGP()) {
       throw new ProvisionException("Bouncy Castle PGP not installed");
     }
-    bind(PublicKeyChecker.class).to(GerritPublicKeyChecker.class);
     bind(PublicKeyStore.class).toProvider(StoreProvider.class);
     DynamicSet.bind(binder(), ReceivePackInitializer.class)
         .to(Initializer.class);
@@ -93,15 +94,22 @@
       if (!ps.isEnableSignedPush()) {
         rp.setSignedPushConfig(null);
         return;
-      }
-      if (signedPushConfig == null) {
+      } else if (signedPushConfig == null) {
         log.error("receive.enableSignedPush is true for project {} but"
             + " false in gerrit.config, so signed push verification is"
             + " disabled", project.get());
+        rp.setSignedPushConfig(null);
+        return;
       }
       rp.setSignedPushConfig(signedPushConfig);
-      rp.setPreReceiveHook(PreReceiveHookChain.newChain(Lists.newArrayList(
-          hook, rp.getPreReceiveHook())));
+
+      List<PreReceiveHook> hooks = new ArrayList<>(3);
+      if (ps.isRequireSignedPush()) {
+        hooks.add(SignedPushPreReceiveHook.Required.INSTANCE);
+      }
+      hooks.add(hook);
+      hooks.add(rp.getPreReceiveHook());
+      rp.setPreReceiveHook(PreReceiveHookChain.newChain(hooks));
     }
   }
 
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
index b2dca8b..cdc3c62 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.gpg;
 
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PreReceiveHook;
 import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceivePack;
 
-import java.io.IOException;
 import java.util.Collection;
 
 /**
@@ -37,18 +36,30 @@
  */
 @Singleton
 public class SignedPushPreReceiveHook implements PreReceiveHook {
-  private final GitRepositoryManager repoManager;
-  private final AllUsersName allUsers;
-  private final PublicKeyChecker keyChecker;
+  public static class Required implements PreReceiveHook {
+    public static final Required INSTANCE = new Required();
+
+    @Override
+    public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+      if (rp.getPushCertificate() == null) {
+        rp.sendMessage("ERROR: Signed push is required");
+        reject(commands, "push cert error");
+      }
+    }
+
+    private Required() {
+    }
+  }
+
+  private final Provider<IdentifiedUser> user;
+  private final GerritPushCertificateChecker.Factory checkerFactory;
 
   @Inject
   public SignedPushPreReceiveHook(
-      GitRepositoryManager repoManager,
-      AllUsersName allUsers,
-      PublicKeyChecker keyChecker) {
-    this.repoManager = repoManager;
-    this.allUsers = allUsers;
-    this.keyChecker = keyChecker;
+      Provider<IdentifiedUser> user,
+      GerritPushCertificateChecker.Factory checkerFactory) {
+    this.user = user;
+    this.checkerFactory = checkerFactory;
   }
 
   @Override
@@ -58,19 +69,11 @@
     if (cert == null) {
       return;
     }
-    PushCertificateChecker checker = new PushCertificateChecker(keyChecker) {
-      @Override
-      protected Repository getRepository() throws IOException {
-        return repoManager.openRepository(allUsers);
-      }
-
-      @Override
-      protected boolean shouldClose(Repository repo) {
-        return true;
-      }
-    };
-    CheckResult result = checker.check(cert);
-    if (!result.isOk()) {
+    CheckResult result = checkerFactory.create(user.get())
+        .setCheckNonce(true)
+        .check(cert)
+        .getCheckResult();
+    if (!isAllowed(result, commands)) {
       for (String problem : result.getProblems()) {
         rp.sendMessage(problem);
       }
@@ -78,6 +81,28 @@
     }
   }
 
+  private static boolean isAllowed(CheckResult result,
+      Collection<ReceiveCommand> commands) {
+    if (onlyMagicBranches(commands)) {
+      // Only pushing magic branches: allow a valid push certificate even if the
+      // key is not ultimately trusted. Assume anyone with Submit permission to
+      // the branch is able to verify during review that the code is legitimate.
+      return result.isOk();
+    } else {
+      // Directly updating one or more refs: require a trusted key.
+      return result.isTrusted();
+    }
+  }
+
+  private static boolean onlyMagicBranches(Iterable<ReceiveCommand> commands) {
+    for (ReceiveCommand c : commands) {
+      if (!MagicBranch.isMagicBranch(c.getRefName())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   private static void reject(Collection<ReceiveCommand> commands,
       String reason) {
     for (ReceiveCommand cmd : commands) {
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index 64b9e85..e6720db 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -16,17 +16,23 @@
 
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.gpg.GerritPushCertificateChecker;
+import com.google.gerrit.gpg.PushCertificateChecker;
 import com.google.gerrit.gpg.server.GpgKeys;
 import com.google.gerrit.gpg.server.PostGpgKeys;
 import com.google.gerrit.server.GpgException;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.api.accounts.GpgApiAdapter;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.bouncycastle.openpgp.PGPException;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificateParser;
 
 import java.io.IOException;
 import java.util.List;
@@ -36,15 +42,18 @@
   private final PostGpgKeys postGpgKeys;
   private final GpgKeys gpgKeys;
   private final GpgKeyApiImpl.Factory gpgKeyApiFactory;
+  private final GerritPushCertificateChecker.Factory pushCertCheckerFactory;
 
   @Inject
   GpgApiAdapterImpl(
       PostGpgKeys postGpgKeys,
       GpgKeys gpgKeys,
-      GpgKeyApiImpl.Factory gpgKeyApiFactory) {
+      GpgKeyApiImpl.Factory gpgKeyApiFactory,
+      GerritPushCertificateChecker.Factory pushCertCheckerFactory) {
     this.postGpgKeys = postGpgKeys;
     this.gpgKeys = gpgKeys;
     this.gpgKeyApiFactory = gpgKeyApiFactory;
+    this.pushCertCheckerFactory = pushCertCheckerFactory;
   }
 
   @Override
@@ -80,4 +89,23 @@
       throw new GpgException(e);
     }
   }
+
+  @Override
+  public PushCertificateInfo checkPushCertificate(String certStr,
+      IdentifiedUser expectedUser) throws GpgException {
+    try {
+      PushCertificate cert = PushCertificateParser.fromString(certStr);
+      PushCertificateChecker.Result result = pushCertCheckerFactory
+          .create(expectedUser)
+          .setCheckNonce(false)
+          .check(cert);
+      PushCertificateInfo info = new PushCertificateInfo();
+      info.certificate = certStr;
+      info.key = GpgKeys.toJson(result.getPublicKey(), result.getCheckResult());
+      return info;
+    } catch (IOException e) {
+      throw new GpgException(e);
+    }
+  }
+
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java
new file mode 100644
index 0000000..932f439
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.gpg.api;
+
+import static com.google.gerrit.gpg.server.GpgKey.GPG_KEY_KIND;
+import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
+
+import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.gpg.server.DeleteGpgKey;
+import com.google.gerrit.gpg.server.GpgKeys;
+import com.google.gerrit.gpg.server.PostGpgKeys;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+
+import java.util.List;
+import java.util.Map;
+
+public class GpgApiModule extends RestApiModule {
+  private final boolean enabled;
+
+  public GpgApiModule(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  @Override
+  protected void configure() {
+    if (!enabled) {
+      bind(GpgApiAdapter.class).to(NoGpgApi.class);
+      return;
+    }
+    bind(GpgApiAdapter.class).to(GpgApiAdapterImpl.class);
+    factory(GpgKeyApiImpl.Factory.class);
+
+    DynamicMap.mapOf(binder(), GPG_KEY_KIND);
+
+    child(ACCOUNT_KIND, "gpgkeys").to(GpgKeys.class);
+    post(ACCOUNT_KIND, "gpgkeys").to(PostGpgKeys.class);
+    get(GPG_KEY_KIND).to(GpgKeys.Get.class);
+    delete(GPG_KEY_KIND).to(DeleteGpgKey.class);
+  }
+
+  private static class NoGpgApi implements GpgApiAdapter {
+    private static final String MSG = "GPG key APIs disabled";
+
+    @Override
+    public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account) {
+      throw new NotImplementedException(MSG);
+    }
+
+    @Override
+    public Map<String, GpgKeyInfo> putGpgKeys(AccountResource account,
+        List<String> add, List<String> delete) {
+      throw new NotImplementedException(MSG);
+    }
+
+    @Override
+    public GpgKeyApi gpgKey(AccountResource account, IdString idStr) {
+      throw new NotImplementedException(MSG);
+    }
+
+    @Override
+    public PushCertificateInfo checkPushCertificate(String certStr,
+        IdentifiedUser expectedUser) {
+      throw new NotImplementedException(MSG);
+    }
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
index b22ca0e..a136007 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -32,7 +32,10 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.gpg.BouncyCastleUtil;
+import com.google.gerrit.gpg.CheckResult;
 import com.google.gerrit.gpg.Fingerprint;
+import com.google.gerrit.gpg.GerritPublicKeyChecker;
+import com.google.gerrit.gpg.PublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -68,14 +71,17 @@
   private final DynamicMap<RestView<GpgKey>> views;
   private final Provider<ReviewDb> db;
   private final Provider<PublicKeyStore> storeProvider;
+  private final GerritPublicKeyChecker.Factory checkerFactory;
 
   @Inject
   GpgKeys(DynamicMap<RestView<GpgKey>> views,
       Provider<ReviewDb> db,
-      Provider<PublicKeyStore> storeProvider) {
+      Provider<PublicKeyStore> storeProvider,
+      GerritPublicKeyChecker.Factory checkerFactory) {
     this.views = views;
     this.db = db;
     this.storeProvider = storeProvider;
+    this.checkerFactory = checkerFactory;
   }
 
   @Override
@@ -155,7 +161,10 @@
           for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
             if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
               found = true;
-              GpgKeyInfo info = toJson(keyRing);
+              GpgKeyInfo info = toJson(
+                  keyRing.getPublicKey(),
+                  checkerFactory.create(rsrc.getUser(), store),
+                  store);
               keys.put(info.id, info);
               info.id = null;
               break;
@@ -173,9 +182,24 @@
 
   @Singleton
   public static class Get implements RestReadView<GpgKey> {
+    private final Provider<PublicKeyStore> storeProvider;
+    private final GerritPublicKeyChecker.Factory checkerFactory;
+
+    @Inject
+    Get(Provider<PublicKeyStore> storeProvider,
+        GerritPublicKeyChecker.Factory checkerFactory) {
+      this.storeProvider = storeProvider;
+      this.checkerFactory = checkerFactory;
+    }
+
     @Override
     public GpgKeyInfo apply(GpgKey rsrc) throws IOException {
-      return toJson(rsrc.getKeyRing());
+      try (PublicKeyStore store = storeProvider.get()) {
+        return toJson(
+            rsrc.getKeyRing().getPublicKey(),
+            checkerFactory.create().setExpectedUser(rsrc.getUser()),
+            store);
+      }
     }
   }
 
@@ -207,23 +231,41 @@
     }
   }
 
-  static GpgKeyInfo toJson(PGPPublicKeyRing keyRing) throws IOException {
-    PGPPublicKey key = keyRing.getPublicKey();
+  public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult)
+      throws IOException {
     GpgKeyInfo info = new GpgKeyInfo();
-    info.id = PublicKeyStore.keyIdToString(key.getKeyID());
-    info.fingerprint = Fingerprint.toString(key.getFingerprint());
-    @SuppressWarnings("unchecked")
-    Iterator<String> userIds = key.getUserIDs();
-    info.userIds = ImmutableList.copyOf(userIds);
-    try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
-        ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
-      // This is not exactly the key stored in the store, but is equivalent. In
-      // particular, it will have a Bouncy Castle version string. The armored
-      // stream reader in PublicKeyStore doesn't give us an easy way to extract
-      // the original ASCII armor.
-      key.encode(aout);
-      info.key = new String(out.toByteArray(), UTF_8);
+
+    if (key != null) {
+      info.id = PublicKeyStore.keyIdToString(key.getKeyID());
+      info.fingerprint = Fingerprint.toString(key.getFingerprint());
+      @SuppressWarnings("unchecked")
+      Iterator<String> userIds = key.getUserIDs();
+      info.userIds = ImmutableList.copyOf(userIds);
+
+      try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+          ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
+        // This is not exactly the key stored in the store, but is equivalent. In
+        // particular, it will have a Bouncy Castle version string. The armored
+        // stream reader in PublicKeyStore doesn't give us an easy way to extract
+        // the original ASCII armor.
+        key.encode(aout);
+        info.key = new String(out.toByteArray(), UTF_8);
+      }
     }
+
+    info.status = checkResult.getStatus();
+    info.problems = checkResult.getProblems();
+
     return info;
   }
+
+  static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker,
+      PublicKeyStore store) throws IOException {
+    return toJson(key, checker.setStore(store).check(key));
+  }
+
+  public static void toJson(GpgKeyInfo info, CheckResult checkResult) {
+    info.status = checkResult.getStatus();
+    info.problems = checkResult.getProblems();
+  }
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 80e3500..91c4494 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -35,12 +35,14 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.gpg.CheckResult;
 import com.google.gerrit.gpg.Fingerprint;
+import com.google.gerrit.gpg.GerritPublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.server.PostGpgKeys.Input;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gwtorm.server.OrmException;
@@ -79,19 +81,19 @@
   private final Provider<PersonIdent> serverIdent;
   private final Provider<ReviewDb> db;
   private final Provider<PublicKeyStore> storeProvider;
-  private final PublicKeyChecker checker;
+  private final GerritPublicKeyChecker.Factory checkerFactory;
   private final AddKeySender.Factory addKeyFactory;
 
   @Inject
   PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent,
       Provider<ReviewDb> db,
       Provider<PublicKeyStore> storeProvider,
-      PublicKeyChecker checker,
+      GerritPublicKeyChecker.Factory checkerFactory,
       AddKeySender.Factory addKeyFactory) {
     this.serverIdent = serverIdent;
     this.db = db;
     this.storeProvider = storeProvider;
-    this.checker = checker;
+    this.checkerFactory = checkerFactory;
     this.addKeyFactory = addKeyFactory;
   }
 
@@ -135,7 +137,7 @@
               return toExtIdKey(fp.get());
             }
           }));
-      return toJson(newKeys, toRemove);
+      return toJson(newKeys, toRemove, store, rsrc.getUser());
     }
   }
 
@@ -191,7 +193,10 @@
       List<String> addedKeys = new ArrayList<>();
       for (PGPPublicKeyRing keyRing : keyRings) {
         PGPPublicKey key = keyRing.getPublicKey();
-        CheckResult result = checker.check(key);
+        // Don't check web of trust; admins can fill in certifications later.
+        CheckResult result = checkerFactory.create(rsrc.getUser(), store)
+            .disableTrust()
+            .check(key);
         if (!result.isOk()) {
           throw new BadRequestException(String.format(
               "Problems with public key %s:\n%s",
@@ -237,13 +242,19 @@
         BaseEncoding.base16().encode(fp));
   }
 
-  private static Map<String, GpgKeyInfo> toJson(
+  private Map<String, GpgKeyInfo> toJson(
       Collection<PGPPublicKeyRing> keys,
-      Set<Fingerprint> deleted) throws IOException {
+      Set<Fingerprint> deleted, PublicKeyStore store, IdentifiedUser user)
+      throws IOException {
+    // Unlike when storing keys, include web-of-trust checks when producing
+    // result JSON, so the user at least knows of any issues.
+    PublicKeyChecker checker = checkerFactory.create(user, store);
     Map<String, GpgKeyInfo> infos =
         Maps.newHashMapWithExpectedSize(keys.size() + deleted.size());
     for (PGPPublicKeyRing keyRing : keys) {
-      GpgKeyInfo info = GpgKeys.toJson(keyRing);
+      PGPPublicKey key = keyRing.getPublicKey();
+      CheckResult result = checker.check(key);
+      GpgKeyInfo info = GpgKeys.toJson(key, result);
       infos.put(info.id, info);
       info.id = null;
     }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index e65ba00..4df9d37 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -14,10 +14,23 @@
 
 package com.google.gerrit.gpg;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.gpg.GerritPublicKeyChecker.toExtIdKey;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
+import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
+import static org.eclipse.jgit.lib.RefUpdate.Result.FORCED;
+import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 import com.google.gerrit.gpg.testutil.TestKey;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
@@ -39,11 +52,23 @@
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificateIdent;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 /** Unit tests for {@link GerritPublicKeyChecker}. */
 public class GerritPublicKeyCheckerTest {
@@ -54,7 +79,7 @@
   private AccountManager accountManager;
 
   @Inject
-  private GerritPublicKeyChecker checker;
+  private GerritPublicKeyChecker.Factory checkerFactory;
 
   @Inject
   private IdentifiedUser.GenericFactory userFactory;
@@ -72,10 +97,18 @@
   private ReviewDb db;
   private Account.Id userId;
   private IdentifiedUser user;
+  private Repository storeRepo;
+  private PublicKeyStore store;
 
   @Before
   public void setUpInjector() throws Exception {
-    Injector injector = Guice.createInjector(new InMemoryModule());
+    Config cfg = InMemoryModule.newDefaultConfig();
+    cfg.setInt("receive", null, "maxTrustDepth", 2);
+    cfg.setStringList("receive", null, "trustedKey", ImmutableList.of(
+        Fingerprint.toString(keyB().getPublicKey().getFingerprint()),
+        Fingerprint.toString(keyD().getPublicKey().getFingerprint())));
+    Injector injector = Guice.createInjector(new InMemoryModule(cfg));
+
     lifecycle = new LifecycleManager();
     lifecycle.add(injector);
     injector.injectMembers(this);
@@ -86,14 +119,14 @@
     userId =
         accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
     Account userAccount = db.accounts().get(userId);
-    // Note: does not match any key in TestKey.
+    // Note: does not match any key in TestKeys.
     userAccount.setPreferredEmail("user@example.com");
     db.accounts().update(ImmutableList.of(userAccount));
     user = reloadUser();
 
     requestContext.setContext(new RequestContext() {
       @Override
-      public CurrentUser getCurrentUser() {
+      public CurrentUser getUser() {
         return user;
       }
 
@@ -102,6 +135,21 @@
         return Providers.of(db);
       }
     });
+
+    storeRepo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+    store = new PublicKeyStore(storeRepo);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    store.close();
+    storeRepo.close();
+  }
+
+  private IdentifiedUser addUser(String name) throws Exception {
+    AuthRequest req = AuthRequest.forUser(name);
+    Account.Id id = accountManager.authenticate(req).getAccountId();
+    return userFactory.create(Providers.of(db), id);
   }
 
   private IdentifiedUser reloadUser() {
@@ -123,23 +171,29 @@
 
   @Test
   public void defaultGpgCertificationMatchesEmail() throws Exception {
-    TestKey key = TestKey.key5();
+    TestKey key = validKeyWithSecondUserId();
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
-        TestKey.key5(),
+        checker.check(key.getPublicKey()), Status.BAD,
         "Key must contain a valid certification for one of the following "
           + "identities:\n"
           + "  gerrit:user\n"
           + "  username:user");
 
     addExternalId("test", "test", "test5@example.com");
-    assertNoProblems(key);
+    checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertNoProblems(checker.check(key.getPublicKey()));
   }
 
   @Test
   public void defaultGpgCertificationDoesNotMatchEmail() throws Exception {
     addExternalId("test", "test", "nobody@example.com");
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
-        TestKey.key5(),
+        checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
         "Key must contain a valid certification for one of the following "
           + "identities:\n"
           + "  gerrit:user\n"
@@ -151,14 +205,18 @@
   @Test
   public void manualCertificationMatchesExternalId() throws Exception {
     addExternalId("foo", "myId", null);
-    assertNoProblems(TestKey.key5());
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertNoProblems(checker.check(validKeyWithSecondUserId().getPublicKey()));
   }
 
   @Test
-  public void manualCertificationDoesNotExternalId() throws Exception {
+  public void manualCertificationDoesNotMatchExternalId() throws Exception {
     addExternalId("foo", "otherId", null);
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
-        TestKey.key5(),
+        checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
         "Key must contain a valid certification for one of the following "
           + "identities:\n"
           + "  foo:otherId\n"
@@ -171,24 +229,225 @@
     db.accountExternalIds().delete(
         db.accountExternalIds().byAccount(user.getAccountId()));
     reloadUser();
+
+    TestKey key = validKeyWithSecondUserId();
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
-        TestKey.key5(),
+        checker.check(key.getPublicKey()), Status.BAD,
         "No identities found for user; check"
           + " http://test/#/settings/web-identities");
+
+    checker = checkerFactory.create()
+        .setStore(store)
+        .disableTrust();
+    assertProblems(
+        checker.check(key.getPublicKey()), Status.BAD,
+        "Key is not associated with any users");
+
+    db.accountExternalIds().insert(Collections.singleton(
+        new AccountExternalId(
+            user.getAccountId(), toExtIdKey(key.getPublicKey()))));
+    reloadUser();
+    assertProblems(
+        checker.check(key.getPublicKey()), Status.BAD,
+        "No identities found for user");
   }
 
-  private void assertNoProblems(TestKey key) throws Exception {
-    assertThat(checker.check(key.getPublicKey()).getProblems()).isEmpty();
+  @Test
+  public void checkValidTrustChainAndCorrectExternalIds() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // The server ultimately trusts B and D.
+    // D and E trust C to be a valid introducer of depth 2.
+    IdentifiedUser userB = addUser("userB");
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), userB);
+    add(keyC(), addUser("userC"));
+    add(keyD(), addUser("userD"));
+    add(keyE(), addUser("userE"));
+
+    // Checker for A, checking A.
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertNoProblems(checkerA.check(keyA.getPublicKey()));
+
+    // Checker for B, checking B. Trust chain and IDs are correct, so the only
+    // problem is with the key itself.
+    PublicKeyChecker checkerB = checkerFactory.create(userB, store);
+    assertProblems(
+        checkerB.check(keyB.getPublicKey()), Status.BAD,
+        "Key is expired");
   }
 
-  private void assertProblems(TestKey key, String... expected)
+  @Test
+  public void checkWithValidKeyButWrongExpectedUserInChecker()
       throws Exception {
-    checkArgument(expected.length > 0);
-    assertThat(checker.check(key.getPublicKey()).getProblems())
-        .containsExactly((Object[]) expected)
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // The server ultimately trusts B and D.
+    // D and E trust C to be a valid introducer of depth 2.
+    IdentifiedUser userB = addUser("userB");
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), userB);
+    add(keyC(), addUser("userC"));
+    add(keyD(), addUser("userD"));
+    add(keyE(), addUser("userE"));
+
+    // Checker for A, checking B.
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertProblems(
+        checkerA.check(keyB.getPublicKey()), Status.BAD,
+        "Key is expired",
+        "Key must contain a valid certification for one of the following"
+            + " identities:\n"
+            + "  gerrit:user\n"
+            + "  mailto:testa@example.com\n"
+            + "  testa@example.com\n"
+            + "  username:user");
+
+    // Checker for B, checking A.
+    PublicKeyChecker checkerB = checkerFactory.create(userB, store);
+    assertProblems(
+        checkerB.check(keyA.getPublicKey()), Status.BAD,
+        "Key must contain a valid certification for one of the following"
+            + " identities:\n"
+            + "  gerrit:userB\n"
+            + "  mailto:testb@example.com\n"
+            + "  testb@example.com\n"
+            + "  username:userB");
+  }
+
+  @Test
+  public void checkTrustChainWithExpiredKey() throws Exception {
+    // A---Bx
+    //
+    // The server ultimately trusts B.
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), addUser("userB"));
+
+    PublicKeyChecker checker = checkerFactory.create(user, store);
+    assertProblems(
+        checker.check(keyA.getPublicKey()), Status.OK,
+        "No path to a trusted key",
+        "Certification by " + keyToString(keyB.getPublicKey())
+            + " is valid, but key is not trusted",
+        "Key D24FE467 used for certification is not in store");
+  }
+
+  @Test
+  public void checkTrustChainUsingCheckerWithoutExpectedKey() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // The server ultimately trusts B and D.
+    // D and E trust C to be a valid introducer of depth 2.
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), addUser("userB"));
+    TestKey keyC = add(keyC(), addUser("userC"));
+    TestKey keyD = add(keyD(), addUser("userD"));
+    TestKey keyE = add(keyE(), addUser("userE"));
+
+    // This checker can check any key, so the only problems come from issues
+    // with the keys themselves, not having invalid user IDs.
+    PublicKeyChecker checker = checkerFactory.create()
+        .setStore(store);
+    assertNoProblems(checker.check(keyA.getPublicKey()));
+    assertProblems(
+        checker.check(keyB.getPublicKey()), Status.BAD,
+        "Key is expired");
+    assertNoProblems(checker.check(keyC.getPublicKey()));
+    assertNoProblems(checker.check(keyD.getPublicKey()));
+    assertProblems(
+        checker.check(keyE.getPublicKey()), Status.BAD,
+        "Key is expired",
+        "No path to a trusted key");
+  }
+
+  @Test
+  public void keyLaterInTrustChainMissingUserId() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C
+    //
+    // The server ultimately trusts B.
+    // C signed A's key but is not in the store.
+    TestKey keyA = add(keyA(), user);
+
+    PGPPublicKeyRing keyRingB = keyB().getPublicKeyRing();
+    PGPPublicKey keyB = keyRingB.getPublicKey();
+    keyB = PGPPublicKey.removeCertification(
+        keyB, (String) keyB.getUserIDs().next());
+    keyRingB = PGPPublicKeyRing.insertPublicKey(keyRingB, keyB);
+    add(keyRingB, addUser("userB"));
+
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertProblems(checkerA.check(keyA.getPublicKey()), Status.OK,
+        "No path to a trusted key",
+        "Certification by " + keyToString(keyB)
+            + " is valid, but key is not trusted",
+        "Key D24FE467 used for certification is not in store");
+  }
+
+  private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception {
+    Account.Id id = user.getAccountId();
+    List<AccountExternalId> newExtIds = new ArrayList<>(2);
+    newExtIds.add(new AccountExternalId(id, toExtIdKey(kr.getPublicKey())));
+
+    @SuppressWarnings("unchecked")
+    String userId = (String) Iterators.getOnlyElement(
+        kr.getPublicKey().getUserIDs(), null);
+    if (userId != null) {
+      String email = PushCertificateIdent.parse(userId).getEmailAddress();
+      assertThat(email).contains("@");
+      AccountExternalId mailto = new AccountExternalId(
+          id, new AccountExternalId.Key(SCHEME_MAILTO, email));
+      mailto.setEmailAddress(email);
+      newExtIds.add(mailto);
+    }
+
+    store.add(kr);
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
+
+    db.accountExternalIds().insert(newExtIds);
+    accountCache.evict(user.getAccountId());
+  }
+
+  private TestKey add(TestKey k, IdentifiedUser user) throws Exception {
+    add(k.getPublicKeyRing(), user);
+    return k;
+  }
+
+  private void assertProblems(CheckResult result, Status expectedStatus,
+      String first, String... rest) throws Exception {
+    List<String> expectedProblems = new ArrayList<>();
+    expectedProblems.add(first);
+    expectedProblems.addAll(Arrays.asList(rest));
+    assertThat(result.getStatus()).isEqualTo(expectedStatus);
+    assertThat(result.getProblems())
+        .containsExactlyElementsIn(expectedProblems)
         .inOrder();
   }
 
+  private void assertNoProblems(CheckResult result) {
+    assertThat(result.getStatus()).isEqualTo(Status.TRUSTED);
+    assertThat(result.getProblems()).isEmpty();
+  }
+
   private void addExternalId(String scheme, String id, String email)
       throws Exception {
     AccountExternalId extId = new AccountExternalId(user.getAccountId(),
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index ebc3e58..742bf1a 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -14,55 +14,384 @@
 
 package com.google.gerrit.gpg;
 
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.keyRevokedByExpiredKeyAfterExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.keyRevokedByExpiredKeyBeforeExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedCompromisedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedNoLongerUsedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.selfRevokedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyF;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyG;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyH;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyI;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyJ;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.gpg.testutil.TestKey;
 
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
 public class PublicKeyCheckerTest {
-  private PublicKeyChecker checker;
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  private InMemoryRepository repo;
+  private PublicKeyStore store;
 
   @Before
   public void setUp() {
-    checker = new PublicKeyChecker();
+    repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+    store = new PublicKeyStore(repo);
+  }
+
+  @After
+  public void tearDown() {
+    if (store != null) {
+      store.close();
+      store = null;
+    }
+    if (repo != null) {
+      repo.close();
+      repo = null;
+    }
   }
 
   @Test
   public void validKey() throws Exception {
-    assertProblems(TestKey.key1());
-  }
-
-  @Test
-  public void wrongKeyId() throws Exception {
-    TestKey k = TestKey.key1();
-    long badId = k.getKeyId() + 1;
-    CheckResult result = checker.check(k.getPublicKey(), badId);
-    assertEquals(
-        Arrays.asList("Public key does not match ID 46328A8D"),
-        result.getProblems());
+    assertNoProblems(validKeyWithoutExpiration());
   }
 
   @Test
   public void keyExpiringInFuture() throws Exception {
-    assertProblems(TestKey.key2());
+    TestKey k = validKeyWithExpiration();
+
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store);
+    assertNoProblems(checker, k);
+
+    checker.setEffectiveTime(parseDate("2015-07-10 12:00:00 -0400"));
+    assertNoProblems(checker, k);
+
+    checker.setEffectiveTime(parseDate("2075-07-10 12:00:00 -0400"));
+    assertProblems(checker, k, "Key is expired");
   }
 
   @Test
-  public void expiredKey() throws Exception {
-    assertProblems(TestKey.key3(), "Key is expired");
+  public void expiredKeyIsExpired() throws Exception {
+    assertProblems(expiredKey(), "Key is expired");
   }
 
   @Test
-  public void selfRevokedKey() throws Exception {
-    assertProblems(TestKey.key4(), "Key is revoked");
+  public void selfRevokedKeyIsRevoked() throws Exception {
+    assertProblems(selfRevokedKey(),
+        "Key is revoked (key material has been compromised)");
   }
 
-  private void assertProblems(TestKey tk, String... expected) throws Exception {
-    CheckResult result = checker.check(tk.getPublicKey(), tk.getKeyId());
-    assertEquals(Arrays.asList(expected), result.getProblems());
+  // Test keys specific to this test are at the bottom of this class. Each test
+  // has a diagram of the trust network, where:
+  //  - The notation M---N indicates N trusts M.
+  //  - An 'x' indicates the key is expired.
+
+  @Test
+  public void trustValidPathLength2() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // D and E trust C to be a valid introducer of depth 2.
+    TestKey ka = add(keyA());
+    TestKey kb = add(keyB());
+    TestKey kc = add(keyC());
+    TestKey kd = add(keyD());
+    TestKey ke = add(keyE());
+    save();
+
+    PublicKeyChecker checker = newChecker(2, kb, kd);
+    assertNoProblems(checker, ka);
+    assertProblems(checker, kb, "Key is expired");
+    assertNoProblems(checker, kc);
+    assertNoProblems(checker, kd);
+    assertProblems(checker, ke, "Key is expired", "No path to a trusted key");
+  }
+
+  @Test
+  public void trustValidPathLength1() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // D and E trust C to be a valid introducer of depth 2.
+    TestKey ka = add(keyA());
+    TestKey kb = add(keyB());
+    TestKey kc = add(keyC());
+    TestKey kd = add(keyD());
+    add(keyE());
+    save();
+
+    PublicKeyChecker checker = newChecker(1, kd);
+    assertProblems(checker, ka,
+        "No path to a trusted key", notTrusted(kb), notTrusted(kc));
+  }
+
+  @Test
+  public void trustCycle() throws Exception {
+    // F---G---F, in a cycle.
+    TestKey kf = add(keyF());
+    TestKey kg = add(keyG());
+    save();
+
+    PublicKeyChecker checker = newChecker(10, keyA());
+    assertProblems(checker, kf,
+        "No path to a trusted key", notTrusted(kg));
+    assertProblems(checker, kg,
+        "No path to a trusted key", notTrusted(kf));
+  }
+
+  @Test
+  public void trustInsufficientDepthInSignature() throws Exception {
+    // H---I---J, but J is only trusted to length 1.
+    TestKey kh = add(keyH());
+    TestKey ki = add(keyI());
+    add(keyJ());
+    save();
+
+    PublicKeyChecker checker = newChecker(10, keyJ());
+
+    // J trusts I to a depth of 1, so I itself is valid, but I's certification
+    // of K is not valid.
+    assertNoProblems(checker, ki);
+    assertProblems(checker, kh,
+        "No path to a trusted key", notTrusted(ki));
+  }
+
+  @Test
+  public void revokedKeyDueToCompromise() throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (key material has been compromised):"
+          + " test6 compromised");
+
+    PGPPublicKeyRing kr = removeRevokers(k.getPublicKeyRing());
+    store.add(kr);
+    save();
+
+    // Key no longer specified as revoker.
+    assertNoProblems(kr.getPublicKey());
+  }
+
+  @Test
+  public void revokedKeyDueToCompromiseRevokesKeyRetroactively()
+      throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    String problem =
+        "Key is revoked (key material has been compromised): test6 compromised";
+    assertProblems(k, problem);
+
+    SimpleDateFormat df = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store)
+        .setEffectiveTime(df.parse("2010-01-01 12:00:00"));
+    assertProblems(checker, k, problem);
+  }
+
+  @Test
+  public void revokedByKeyNotPresentInStore() throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (key material has been compromised):"
+          + " test6 compromised");
+  }
+
+  @Test
+  public void revokedKeyDueToNoLongerBeingUsed() throws Exception {
+    TestKey k = add(revokedNoLongerUsedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (retired and no longer valid): test7 not used");
+  }
+
+  @Test
+  public void revokedKeyDueToNoLongerBeingUsedDoesNotRevokeKeyRetroactively()
+      throws Exception {
+    TestKey k = add(revokedNoLongerUsedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (retired and no longer valid): test7 not used");
+
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store)
+        .setEffectiveTime(parseDate("2010-01-01 12:00:00 -0400"));
+    assertNoProblems(checker, k);
+  }
+
+  @Test
+  public void keyRevokedByExpiredKeyAfterExpirationIsNotRevoked()
+      throws Exception {
+    TestKey k = add(keyRevokedByExpiredKeyAfterExpiration());
+    add(expiredKey());
+    save();
+
+    PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
+    assertNoProblems(checker, k);
+  }
+
+  @Test
+  public void keyRevokedByExpiredKeyBeforeExpirationIsRevoked()
+      throws Exception {
+    TestKey k = add(keyRevokedByExpiredKeyBeforeExpiration());
+    add(expiredKey());
+    save();
+
+    PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
+    assertProblems(checker, k,
+        "Key is revoked (retired and no longer valid): test9 not used");
+
+    // Set time between key creation and revocation.
+    checker.setEffectiveTime(parseDate("2005-08-01 13:00:00 -0400"));
+    assertNoProblems(checker, k);
+  }
+
+  private PGPPublicKeyRing removeRevokers(PGPPublicKeyRing kr) {
+    PGPPublicKey k = kr.getPublicKey();
+    @SuppressWarnings("unchecked")
+    Iterator<PGPSignature> sigs = k.getSignaturesOfType(DIRECT_KEY);
+    while (sigs.hasNext()) {
+      PGPSignature sig = sigs.next();
+      if (sig.getHashedSubPackets().hasSubpacket(REVOCATION_KEY)) {
+        k = PGPPublicKey.removeCertification(k, sig);
+      }
+    }
+    return PGPPublicKeyRing.insertPublicKey(kr, k);
+  }
+
+  private PublicKeyChecker newChecker(int maxTrustDepth, TestKey... trusted) {
+    Map<Long, Fingerprint> fps = new HashMap<>();
+    for (TestKey k : trusted) {
+      Fingerprint fp = new Fingerprint(k.getPublicKey().getFingerprint());
+      fps.put(fp.getId(), fp);
+    }
+    return new PublicKeyChecker()
+        .enableTrust(maxTrustDepth, fps)
+        .setStore(store);
+  }
+
+  private TestKey add(TestKey k) {
+    store.add(k.getPublicKeyRing());
+    return k;
+  }
+
+  private void save() throws Exception {
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    RefUpdate.Result result = store.save(cb);
+    switch (result) {
+      case NEW:
+      case FAST_FORWARD:
+      case FORCED:
+        break;
+      default:
+        throw new AssertionError(result);
+    }
+  }
+
+  private void assertProblems(PublicKeyChecker checker, TestKey k,
+      String first, String... rest) {
+    CheckResult result = checker.setStore(store)
+        .check(k.getPublicKey());
+    assertEquals(list(first, rest), result.getProblems());
+  }
+
+  private void assertNoProblems(PublicKeyChecker checker, TestKey k) {
+    CheckResult result = checker.setStore(store)
+        .check(k.getPublicKey());
+    assertEquals(Collections.emptyList(), result.getProblems());
+  }
+
+  private void assertProblems(TestKey tk, String first, String... rest) {
+    assertProblems(tk.getPublicKey(), first, rest);
+  }
+
+  private void assertNoProblems(TestKey tk) {
+    assertNoProblems(tk.getPublicKey());
+  }
+
+  private void assertProblems(PGPPublicKey k, String first, String... rest) {
+    CheckResult result = new PublicKeyChecker()
+        .setStore(store)
+        .check(k);
+    assertEquals(list(first, rest), result.getProblems());
+  }
+
+  private void assertNoProblems(PGPPublicKey k) {
+    CheckResult result = new PublicKeyChecker()
+        .setStore(store)
+        .check(k);
+    assertEquals(Collections.emptyList(), result.getProblems());
+  }
+
+  private static String notTrusted(TestKey k) {
+    return "Certification by " + keyToString(k.getPublicKey())
+        + " is valid, but key is not trusted";
+  }
+
+  private static Date parseDate(String str) throws Exception {
+    return new SimpleDateFormat("YYYY-MM-dd HH:mm:ss Z").parse(str);
+  }
+
+  private static List<String> list(String first, String[] rest) {
+    List<String> all = new ArrayList<>();
+    all.add(first);
+    all.addAll(Arrays.asList(rest));
+    return all;
   }
 }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
index d936a31..9c0a908 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -18,6 +18,9 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.gpg.PublicKeyStore.keyObjectId;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -60,13 +63,13 @@
 
   @Test
   public void testKeyIdToString() throws Exception {
-    PGPPublicKey key = TestKey.key1().getPublicKey();
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
     assertEquals("46328A8C", keyIdToString(key.getKeyID()));
   }
 
   @Test
   public void testKeyToString() throws Exception {
-    PGPPublicKey key = TestKey.key1().getPublicKey();
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
     assertEquals("46328A8C Testuser One <test1@example.com>"
           + " (04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C)",
         keyToString(key));
@@ -74,7 +77,7 @@
 
   @Test
   public void testKeyObjectId() throws Exception {
-    PGPPublicKey key = TestKey.key1().getPublicKey();
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
     String objId = keyObjectId(key.getKeyID()).name();
     assertEquals("ed0625dc46328a8c000000000000000000000000", objId);
     assertEquals(keyIdToString(key.getKeyID()).toLowerCase(),
@@ -83,13 +86,13 @@
 
   @Test
   public void testGet() throws Exception {
-    TestKey key1 = TestKey.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         .add(keyObjectId(key1.getKeyId()).name(),
           key1.getPublicKeyArmored())
         .create();
-    TestKey key2 = TestKey.key2();
+    TestKey key2 = validKeyWithExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         .add(keyObjectId(key2.getKeyId()).name(),
@@ -102,8 +105,8 @@
 
   @Test
   public void testGetMultiple() throws Exception {
-    TestKey key1 = TestKey.key1();
-    TestKey key2 = TestKey.key2();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         .add(keyObjectId(key1.getKeyId()).name(),
@@ -116,8 +119,8 @@
 
   @Test
   public void save() throws Exception {
-    TestKey key1 = TestKey.key1();
-    TestKey key2 = TestKey.key2();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
     store.add(key1.getPublicKeyRing());
     store.add(key2.getPublicKeyRing());
 
@@ -129,8 +132,8 @@
 
   @Test
   public void saveAppendsToExistingList() throws Exception {
-    TestKey key1 = TestKey.key1();
-    TestKey key2 = TestKey.key2();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         // Mismatched for this key ID, but we can still read it out.
@@ -160,7 +163,7 @@
 
   @Test
   public void updateExisting() throws Exception {
-    TestKey key5 = TestKey.key5();
+    TestKey key5 = validKeyWithSecondUserId();
     PGPPublicKeyRing keyRing = key5.getPublicKeyRing();
     PGPPublicKey key = keyRing.getPublicKey();
     store.add(keyRing);
@@ -184,7 +187,7 @@
 
   @Test
   public void remove() throws Exception {
-    TestKey key1 = TestKey.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
     assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
     assertKeys(key1.getKeyId(), key1);
@@ -196,11 +199,11 @@
 
   @Test
   public void removeNonexisting() throws Exception {
-    TestKey key1 = TestKey.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
     assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
 
-    TestKey key2 = TestKey.key2();
+    TestKey key2 = validKeyWithExpiration();
     store.remove(key2.getPublicKey().getFingerprint());
     assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));
     assertKeys(key1.getKeyId(), key1);
@@ -208,7 +211,7 @@
 
   @Test
   public void addThenRemove() throws Exception {
-    TestKey key1 = TestKey.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
     store.remove(key1.getPublicKey().getFingerprint());
     assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
index 8a633ae..ee07d55 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.gpg;
 
-import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 
@@ -26,11 +28,14 @@
 import org.bouncycastle.bcpg.BCPGOutputStream;
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.PGPSignatureGenerator;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
 import org.bouncycastle.openpgp.PGPUtil;
 import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.PushCertificateIdent;
@@ -43,57 +48,80 @@
 import java.io.ByteArrayOutputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
 
 public class PushCertificateCheckerTest {
-  private TestRepository<?> tr;
+  private InMemoryRepository repo;
+  private PublicKeyStore store;
   private SignedPushConfig signedPushConfig;
   private PushCertificateChecker checker;
 
   @Before
   public void setUp() throws Exception {
-    TestKey key1 = TestKey.key1();
-    TestKey key3 = TestKey.key3();
-    tr = new TestRepository<>(new InMemoryRepository(
-        new DfsRepositoryDescription("repo")));
-    tr.branch(REFS_GPG_KEYS).commit()
-        .add(PublicKeyStore.keyObjectId(key1.getPublicKey().getKeyID()).name(),
-            key1.getPublicKeyArmored())
-        .add(PublicKeyStore.keyObjectId(key3.getPublicKey().getKeyID()).name(),
-            key3.getPublicKeyArmored())
-        .create();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key3 = expiredKey();
+    repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+    store = new PublicKeyStore(repo);
+    store.add(key1.getPublicKeyRing());
+    store.add(key3.getPublicKeyRing());
+
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    assertEquals(RefUpdate.Result.NEW, store.save(cb));
+
     signedPushConfig = new SignedPushConfig();
     signedPushConfig.setCertNonceSeed("sekret");
     signedPushConfig.setCertNonceSlopLimit(60 * 24);
+    checker = newChecker(true);
+  }
 
-    checker = new PushCertificateChecker(new PublicKeyChecker()) {
+  private PushCertificateChecker newChecker(boolean checkNonce) {
+    PublicKeyChecker keyChecker = new PublicKeyChecker().setStore(store);
+    return new PushCertificateChecker(keyChecker) {
       @Override
       protected Repository getRepository() {
-        return tr.getRepository();
+        return repo;
       }
 
       @Override
       protected boolean shouldClose(Repository repo) {
         return false;
       }
-    };
+    }.setCheckNonce(checkNonce);
   }
 
   @Test
   public void validCert() throws Exception {
-    PushCertificate cert = newSignedCert(validNonce(), TestKey.key1());
-    assertProblems(cert);
+    PushCertificate cert =
+        newSignedCert(validNonce(), validKeyWithoutExpiration());
+    assertNoProblems(cert);
   }
 
   @Test
   public void invalidNonce() throws Exception {
-    PushCertificate cert = newSignedCert("invalid-nonce", TestKey.key1());
+    PushCertificate cert =
+        newSignedCert("invalid-nonce", validKeyWithoutExpiration());
     assertProblems(cert, "Invalid nonce");
   }
 
   @Test
+  public void invalidNonceNotChecked() throws Exception {
+    checker = newChecker(false);
+    PushCertificate cert =
+        newSignedCert("invalid-nonce", validKeyWithoutExpiration());
+    assertNoProblems(cert);
+  }
+
+  @Test
   public void missingKey() throws Exception {
-    TestKey key2 = TestKey.key2();
+    TestKey key2 = validKeyWithExpiration();
     PushCertificate cert = newSignedCert(validNonce(), key2);
     assertProblems(cert,
         "No public keys found for key ID " + keyIdToString(key2.getKeyId()));
@@ -101,20 +129,34 @@
 
   @Test
   public void invalidKey() throws Exception {
-    TestKey key3 = TestKey.key3();
+    TestKey key3 = expiredKey();
     PushCertificate cert = newSignedCert(validNonce(), key3);
     assertProblems(cert,
         "Invalid public key " + keyToString(key3.getPublicKey())
           + ":\n  Key is expired");
   }
 
+  @Test
+  public void signatureByExpiredKeyBeforeExpiration() throws Exception {
+    TestKey key3 = expiredKey();
+    Date now = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss Z")
+        .parse("2005-07-10 12:00:00 -0400");
+    PushCertificate cert = newSignedCert(validNonce(), key3, now);
+    assertNoProblems(cert);
+  }
+
   private String validNonce() {
     return signedPushConfig.getNonceGenerator()
-        .createNonce(tr.getRepository(), System.currentTimeMillis() / 1000);
+        .createNonce(repo, System.currentTimeMillis() / 1000);
   }
 
   private PushCertificate newSignedCert(String nonce, TestKey signingKey)
       throws Exception {
+    return newSignedCert(nonce, signingKey, null);
+  }
+
+  private PushCertificate newSignedCert(String nonce, TestKey signingKey,
+      Date now) throws Exception {
     PushCertificateIdent ident = new PushCertificateIdent(
         signingKey.getFirstUserId(), System.currentTimeMillis(), -7 * 60);
     String payload = "certificate version 0.1\n"
@@ -128,6 +170,14 @@
     PGPSignatureGenerator gen = new PGPSignatureGenerator(
         new BcPGPContentSignerBuilder(
           signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1));
+
+    if (now != null) {
+      PGPSignatureSubpacketGenerator subGen =
+          new PGPSignatureSubpacketGenerator();
+      subGen.setSignatureCreationTime(false, now);
+      gen.setHashedSubpackets(subGen.generate());
+    }
+
     gen.init(PGPSignature.BINARY_DOCUMENT, signingKey.getPrivateKey());
     gen.update(payload.getBytes(UTF_8));
     PGPSignature sig = gen.generate();
@@ -142,13 +192,21 @@
     Reader reader =
         new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)));
     PushCertificateParser parser =
-        new PushCertificateParser(tr.getRepository(), signedPushConfig);
+        new PushCertificateParser(repo, signedPushConfig);
     return parser.parse(reader);
   }
 
-  private void assertProblems(PushCertificate cert, String... expected)
-      throws Exception {
-    CheckResult result = checker.check(cert);
-    assertEquals(Arrays.asList(expected), result.getProblems());
+  private void assertProblems(PushCertificate cert, String first,
+      String... rest) throws Exception {
+    List<String> expected = new ArrayList<>();
+    expected.add(first);
+    expected.addAll(Arrays.asList(rest));
+    CheckResult result = checker.check(cert).getCheckResult();
+    assertEquals(expected, result.getProblems());
+  }
+
+  private void assertNoProblems(PushCertificate cert) {
+    CheckResult result = checker.check(cert).getCheckResult();
+    assertEquals(Collections.emptyList(), result.getProblems());
   }
 }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java
index 614818e..494cb2d 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java
@@ -16,8 +16,6 @@
 
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 
-import com.google.common.collect.ImmutableList;
-
 import org.bouncycastle.bcpg.ArmoredInputStream;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPrivateKey;
@@ -34,536 +32,12 @@
 import java.io.IOException;
 
 public class TestKey {
-  public static ImmutableList<TestKey> allValidKeys() {
-    return ImmutableList.of(key1(), key2(), key5());
-  }
-
-  /**
-   * A valid key with no expiration.
-   *
-   * <pre>
-   * pub   2048R/46328A8C 2015-07-08
-   *       Key fingerprint = 04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C
-   * uid                  Testuser One &lt;test1@example.com&gt;
-   * sub   2048R/F0AF69C0 2015-07-08
-   * </pre>
-   */
-  public static TestKey key1() {
-    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "mQENBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
-        + "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
-        + "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
-        + "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
-        + "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
-        + "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAG0IFRlc3R1c2VyIE9uZSA8\n"
-        + "dGVzdDFAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJ\n"
-        + "CgsEFgIDAQIeAQIXgAAKCRDtBiXcRjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8Lq\n"
-        + "yUpBrDp3P06QDGpKGFMAovBuh+NLH76VKNIzQLQC8rdTj651fLcLMuJ1enQ3Rblg\n"
-        + "RKr1oc+wqqtFHr4QyOQjE/N3C9GQjEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMx\n"
-        + "jRcHbM9KQnsE5Z4fh4wmN5ynG+5nbaF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX\n"
-        + "7Qkzze+scAlc9E/EWRJQIFcxnxV/SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjy\n"
-        + "W0lGHnh/ZqH6XGVcGUaJZZ2uHTck1+czuVVShNcXPW1W20T6E9UqzHbJHN0guQEN\n"
-        + "BFWdTIkBCACoLVdPr3gpQwzI+2NGXjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjN\n"
-        + "vYkS/+/oGtVEmiYOiAVTwmkjCYkKGDgNcCiJVekiPAN6JryVv488wRc999b5LpFE\n"
-        + "fhLGwI0YxjcS4KFFnpMC3wSb6tJUnHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIb\n"
-        + "nuyrk3ydEcS4ZeGD+w+taIxMc9F1DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3m\n"
-        + "rBCo97sE95yKcq98ZMIWuQtTcEccZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11Vl\n"
-        + "IQ9QFSj6ruqoKrYvNZuDDLD1lHvZPD4/ABEBAAGJAR8EGAECAAkFAlWdTIkCGwwA\n"
-        + "CgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUsj/16fGiF\n"
-        + "rRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl+xqsgpEj\n"
-        + "Fhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs3YI19Ci/\n"
-        + "FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnxqhH4wfHB\n"
-        + "PGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1H2PPSxrA\n"
-        + "0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
-        + "=o/aU\n"
-        + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "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\n"
-        + "=MuAn\n"
-        + "-----END PGP PRIVATE KEY BLOCK-----\n");
-  }
-
-  /**
-   * A valid key expiring in 2065.
-   *
-   * <pre>
-   * pub   2048R/378A0AED 2015-07-08 [expires: 2065-06-25]
-   *       Key fingerprint = C378 369A CBCD 34CC 138D  90B1 4531 1A6F 378A 0AED
-   * uid                  Testuser Two &lt;test2@example.com&gt;
-   * sub   2048R/46D4F204 2015-07-08 [expires: 2065-06-25]
-   * </pre>
-   */
-  public static final TestKey key2() {
-    return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "mQENBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
-        + "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
-        + "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
-        + "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
-        + "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
-        + "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAG0IFRlc3R1c2VyIFR3byA8\n"
-        + "dGVzdDJAZXhhbXBsZS5jb20+iQE+BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcD\n"
-        + "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0d\n"
-        + "UdvAXeBx7DwOAnAodis9ZVqChb7RxcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6\n"
-        + "bgW+1WOB1tZSDVxwL1PnZFw/SyADRIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZ\n"
-        + "FMTFUr2SPscXk1k7muS+ZfEFwNPD4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT\n"
-        + "449CYoq8XBMBfvyWl/LLpw0r3JI6pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T\n"
-        + "8TKDGwwiuwiiT3SfkFSVdcjKulRuXSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iu\n"
-        + "RHSOuQENBFWdTP8BCADhhGxAA0pX5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnR\n"
-        + "tBScgKZnP0sjRTYEUIwmZuseHMBohtVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIe\n"
-        + "qCrm/6aejbFcQOpxe6U29KJRCAxuwNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZ\n"
-        + "oIvpIe9tZH4aXitCY2MCQH+hTyCyNBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+9\n"
-        + "7HCe042GIq65h0apgujyjhJidjch5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xP\n"
-        + "d9MncY5Q/eH+hn96694k5bckottSyGm/3f2Ihfj1ABEBAAGJASUEGAECAA8FAlWd\n"
-        + "TP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb1nsgRMgV\n"
-        + "YoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFyxo6lLHw9\n"
-        + "NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q3uwvP5fb\n"
-        + "fSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfqlOG7SPvM\n"
-        + "NmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk13ynADO+v\n"
-        + "EOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN9A==\n"
-        + "=1e/A\n"
-        + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "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\n"
-        + "9A==\n"
-        + "=qbV3\n"
-        + "-----END PGP PRIVATE KEY BLOCK-----\n");
-  }
-
-  /**
-   * A key that expired in 2006.
-   *
-   * <pre>
-   * pub   2048R/17DE1ACD 2005-07-08 [expired: 2006-07-08]
-   *       Key fingerprint = 1D9E EB79 DD38 B049 939D  9CAF 3CEC 781B 17DE 1ACD
-   * uid                  Testuser Three &lt;test3@example.com&gt;
-   * </pre>
-   */
-  public static final TestKey key3() {
-    return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "mQENBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
-        + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
-        + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
-        + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
-        + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
-        + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAG0IlRlc3R1c2VyIFRocmVl\n"
-        + "IDx0ZXN0M0BleGFtcGxlLmNvbT6JAT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkI\n"
-        + "BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFC\n"
-        + "ECWLrcOeimuvwbmkonNzOkvKbGXl73GStISAksRWAHBQED1rEPC0NkFCDeVZO7df\n"
-        + "SYLlsqKwV6uSh05Ra0F5XeniC12YpAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCu\n"
-        + "R+8sNu/oecMRcFK4S9NaApi3vdqBNhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSk\n"
-        + "qcPfKZmocNXdgLV5Q80n3hc2y2nrl+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5\n"
-        + "btBW2L0UHtoEyiqkRfD6lX2laSLQmA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/\n"
-        + "2thO41K5AQ0EQs6nRQEIAM/833UHK1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3be\n"
-        + "eE4sh1NG5DbRCdo6iacZLarWr3FDz7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5F\n"
-        + "p5u2R4WF546bWqX45xPdLfHVTPyWB9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihw\n"
-        + "dxLsxaga+QmaL0bAR+dRcO6ucj7TDQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9Aj\n"
-        + "FoumMZ6l+k30sSdjSjpBMsNvPos0dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELp\n"
-        + "KgujZ2sKC9Nm395u6Q4cqUWihzb/Y7rIRuNHJarI7vUAEQEAAYkBJQQYAQIADwUC\n"
-        + "Qs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs7mvEWJI/\n"
-        + "1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Feyxb2rjtb\n"
-        + "NrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt10RaYR8VE\n"
-        + "ZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGdUt8U1Kq9\n"
-        + "OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/jPj5FUEU\n"
-        + "kE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6QHDJb\n"
-        + "=d/Xp\n"
-        + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "lQOYBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
-        + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
-        + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
-        + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
-        + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
-        + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAEAB/4hGI3ckkLMTjRVa7G1\n"
-        + "YYSv4sr8dHXz0CVpZXKOo+Stef3Z4pZTK/BcXOdROvaXooD+EheAs6Yn4fpnT+/K\n"
-        + "IB7ZAx6C0OL8vz17gbPuBFltMZ/COUwaCi/gFCUfWQgqRp/SdHaOfCIuTxpAkDSS\n"
-        + "tpmWJ8eDDSFudMpgweb+SrF9DkCwp+FgUbzDRzO1aqzuu8PGihCHQt/pkhNHQ63/\n"
-        + "srDDqk6lIxxZHhv9+ucr3plDuijkvAa5/QDudQlucKDLtTPSD40UcqYnpg/V/RJU\n"
-        + "eBK0ZXmCIHpG9beHW/xdlwrK3eY4Z2sVDMm9TeeHmRYOCr5wQCyeLpMdAt0Ijk6a\n"
-        + "nINhBADI2lRodgnLvUKbOvVocz8WQjG1IXlL8iXSNuuHONijPXZiWh7XdkNxr9fm\n"
-        + "jRqzvZzYsWGT6MnirX2eXaEWJsWJHxTxJuiuOk0V/iGnV/d+jFduoKXNmB5k/ZB3\n"
-        + "6zySi7+STKNyIvnMATVsRoI/cNUwfmx53m6trFg581CnSiA82QQA4kSPw9OXmTKj\n"
-        + "ctlHrWsapWu+66pDVZw62lW6lvrd7t+m8liNb6VJuTnwIKVXJOQtUo1+GSMs0+YK\n"
-        + "wnd9FGq4jT8l0qBO4K/8B1HxppLC2S0ntC+CusxWMUDbdC2xg+G2W3oLwq3iamgz\n"
-        + "LvPTy1Pzs9PqDd6FXIdzieFy6J8W1+sEAKS3vjh7Z/PIVULZhdaohAd5Igd67S/Z\n"
-        + "BMWYNbBuJTnnb7DiOllLZSd2lR7IAKPKsUd6UY8uskOxI81hI116zNx17mIGFIIq\n"
-        + "DdDgRbvzMNEgNlOxg/BD01kXOS4fhnT2F6ca3VGTgUtOdcdF3M9MtePWQLBzEDPz\n"
-        + "8nx3O20HDupuQmG0IlRlc3R1c2VyIFRocmVlIDx0ZXN0M0BleGFtcGxlLmNvbT6J\n"
-        + "AT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheA\n"
-        + "AAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFCECWLrcOeimuvwbmkonNzOkvKbGXl\n"
-        + "73GStISAksRWAHBQED1rEPC0NkFCDeVZO7dfSYLlsqKwV6uSh05Ra0F5XeniC12Y\n"
-        + "pAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCuR+8sNu/oecMRcFK4S9NaApi3vdqB\n"
-        + "NhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSkqcPfKZmocNXdgLV5Q80n3hc2y2nr\n"
-        + "l+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5btBW2L0UHtoEyiqkRfD6lX2laSLQ\n"
-        + "mA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/2thO41KdA5gEQs6nRQEIAM/833UH\n"
-        + "K1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3beeE4sh1NG5DbRCdo6iacZLarWr3FD\n"
-        + "z7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5Fp5u2R4WF546bWqX45xPdLfHVTPyW\n"
-        + "B9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihwdxLsxaga+QmaL0bAR+dRcO6ucj7T\n"
-        + "DQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9AjFoumMZ6l+k30sSdjSjpBMsNvPos0\n"
-        + "dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELpKgujZ2sKC9Nm395u6Q4cqUWihzb/\n"
-        + "Y7rIRuNHJarI7vUAEQEAAQAH+gNBKDf7FDzwdM37Sz8Ej7OsPcIbekzPcOpV3mzM\n"
-        + "u/NIuOY0QSvW7KRE8hwFlXjVZocJU/Z4Qqw+12pN55LusiRUrOq8eKuJIbl4QikI\n"
-        + "Dea8XUqM+CKJPV3YZXs6YVdIuzrRBSLgsB/Glff5JlzkEjsRYVmmnto8edETL/MK\n"
-        + "S9ClJqQiFKE4b01+Eh9oB/DfxzsiEf/a+rdRnWRh/jtpEwgeXcfmjhf+0zrzChu2\n"
-        + "ylQQ5QOuwQNKJP6DvRu/W5pOaKH9tPDR31SccDJDdnDUzBD7oSsXl06DcfMNEa8q\n"
-        + "PaNHLDDRNnqTEhwYSJ4r2emDFMxg7Kky+aatUNjAYk9vkgMEANnvumgr6/KCLWKc\n"
-        + "D3fZE09N7BveGBBDQBYNGPFtx60WbKrSY3e2RSfgWbyEXkzwm1VlB2869T1we0rL\n"
-        + "z6eV/TK5rrJQxJFHZ/anMxbQY0sCiOgqi6PKT03RTpA2N803hTym+oypy+5T6BFM\n"
-        + "rtjXvwIZN/BgAE2JjA70crTAd1mvBAD0UFNAU9oE7K7sgDbni4EhxmDyaviBHfxV\n"
-        + "PJP1ICUXAcEzAsz2T/L5TqZUD+LfYIkbf8wk2/mPZFfrCrQgCrzWn7KV1SHXkhf4\n"
-        + "4Sg6Y6p0g0Jl3mWRPiQ6ALlOVQIkp5V8z4b0hTF2c4oct1Pzaeq+ZkahyvrhW06P\n"
-        + "iaucRZb+mwP/aVTpkd4n/FyKCcbf9/KniYJ+Ou1OunsBQr/jzN+r0PKCb8l/ksig\n"
-        + "i/M0NGetemq9CxYsJDAyJs1aO4SWgx5LbfcMmyXDuJ3sL0ztFLOES31Mih3ZJebg\n"
-        + "xPpj2bB/67i2zeYRcjxQ116y23gOa2TWM8EE4TW7F/mQjw4fIPJ93ClBMIkBJQQY\n"
-        + "AQIADwUCQs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs\n"
-        + "7mvEWJI/1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Fe\n"
-        + "yxb2rjtbNrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt1\n"
-        + "0RaYR8VEZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGd\n"
-        + "Ut8U1Kq9OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/\n"
-        + "jPj5FUEUkE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6Q\n"
-        + "HDJb\n"
-        + "=RrXv\n"
-        + "-----END PGP PRIVATE KEY BLOCK-----\n");
-  }
-
-  /**
-   * A self-revoked key with no expiration.
-   *
-   * <pre>
-   * pub   2048R/7CA87821 2015-07-08 [revoked: 2015-07-08]
-   *       Key fingerprint = E328 CAB1 1F7E B1BC 1451  ABA5 0855 2A17 7CA8 7821
-   * uid                  Testuser Four &lt;test4@example.com&gt;
-   * </pre>
-   */
-  public static final TestKey key4() {
-    return new TestKey(
-        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "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\n"
-        + "=477N\n"
-        + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "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\n"
-        + "=5aNq\n"
-        + "-----END PGP PRIVATE KEY BLOCK-----\n");
-  }
-
-  /**
-   * A key with an additional user ID.
-   *
-   * <pre>
-   * pub   2048R/98C51DBF 2015-07-30
-   *       Key fingerprint = 42B3 294D 1924 D7EB AF4A  A99F 5024 BB44 98C5 1DBF
-   * uid                  foo:myId
-   * uid                  Testuser Five <test5@example.com>
-   * sub   2048R/C781A9E3 2015-07-30
-   * </pre>
-   */
-  public static TestKey key5() {
-    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "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\n"
-        + "54w16Vp/SVViJs7nTMSug0WREyd2\n"
-        + "=ldwB\n"
-        + "-----END PGP PUBLIC KEY BLOCK-----\n",
-        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
-        + "Version: GnuPG v1\n"
-        + "\n"
-        + "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\n"
-        + "=uND5\n"
-        + "-----END PGP PRIVATE KEY BLOCK-----\n");
-  }
-
-  // TODO(dborowitz): Figure out how to get gpg to revoke a key for someone
-  // else.
-
   private final String pubArmored;
   private final String secArmored;
   private final PGPPublicKeyRing pubRing;
   private final PGPSecretKeyRing secRing;
 
-  private TestKey(String pubArmored, String secArmored) {
+  public TestKey(String pubArmored, String secArmored) {
     this.pubArmored = pubArmored;
     this.secArmored = secArmored;
     BcKeyFingerprintCalculator fc = new BcKeyFingerprintCalculator();
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
new file mode 100644
index 0000000..ad944c5
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
@@ -0,0 +1,1028 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.gpg.testutil;
+
+import com.google.common.collect.ImmutableList;
+
+/** Common test keys used by a variety of tests. */
+public class TestKeys {
+  public static ImmutableList<TestKey> allValidKeys() {
+    return ImmutableList.of(
+        validKeyWithoutExpiration(),
+        validKeyWithExpiration(),
+        validKeyWithSecondUserId());
+  }
+
+  /**
+   * A valid key with no expiration.
+   *
+   * <pre>
+   * pub   2048R/46328A8C 2015-07-08
+   *       Key fingerprint = 04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C
+   * uid                  Testuser One &lt;test1@example.com&gt;
+   * sub   2048R/F0AF69C0 2015-07-08
+   * </pre>
+   */
+  public static TestKey validKeyWithoutExpiration() {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
+        + "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
+        + "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
+        + "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
+        + "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
+        + "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAG0IFRlc3R1c2VyIE9uZSA8\n"
+        + "dGVzdDFAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJ\n"
+        + "CgsEFgIDAQIeAQIXgAAKCRDtBiXcRjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8Lq\n"
+        + "yUpBrDp3P06QDGpKGFMAovBuh+NLH76VKNIzQLQC8rdTj651fLcLMuJ1enQ3Rblg\n"
+        + "RKr1oc+wqqtFHr4QyOQjE/N3C9GQjEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMx\n"
+        + "jRcHbM9KQnsE5Z4fh4wmN5ynG+5nbaF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX\n"
+        + "7Qkzze+scAlc9E/EWRJQIFcxnxV/SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjy\n"
+        + "W0lGHnh/ZqH6XGVcGUaJZZ2uHTck1+czuVVShNcXPW1W20T6E9UqzHbJHN0guQEN\n"
+        + "BFWdTIkBCACoLVdPr3gpQwzI+2NGXjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjN\n"
+        + "vYkS/+/oGtVEmiYOiAVTwmkjCYkKGDgNcCiJVekiPAN6JryVv488wRc999b5LpFE\n"
+        + "fhLGwI0YxjcS4KFFnpMC3wSb6tJUnHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIb\n"
+        + "nuyrk3ydEcS4ZeGD+w+taIxMc9F1DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3m\n"
+        + "rBCo97sE95yKcq98ZMIWuQtTcEccZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11Vl\n"
+        + "IQ9QFSj6ruqoKrYvNZuDDLD1lHvZPD4/ABEBAAGJAR8EGAECAAkFAlWdTIkCGwwA\n"
+        + "CgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUsj/16fGiF\n"
+        + "rRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl+xqsgpEj\n"
+        + "Fhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs3YI19Ci/\n"
+        + "FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnxqhH4wfHB\n"
+        + "PGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1H2PPSxrA\n"
+        + "0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
+        + "=o/aU\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=MuAn\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A valid key expiring in 2065.
+   *
+   * <pre>
+   * pub   2048R/378A0AED 2015-07-08 [expires: 2065-06-25]
+   *       Key fingerprint = C378 369A CBCD 34CC 138D  90B1 4531 1A6F 378A 0AED
+   * uid                  Testuser Two &lt;test2@example.com&gt;
+   * sub   2048R/46D4F204 2015-07-08 [expires: 2065-06-25]
+   * </pre>
+   */
+  public static final TestKey validKeyWithExpiration() {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
+        + "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
+        + "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
+        + "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
+        + "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
+        + "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAG0IFRlc3R1c2VyIFR3byA8\n"
+        + "dGVzdDJAZXhhbXBsZS5jb20+iQE+BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcD\n"
+        + "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0d\n"
+        + "UdvAXeBx7DwOAnAodis9ZVqChb7RxcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6\n"
+        + "bgW+1WOB1tZSDVxwL1PnZFw/SyADRIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZ\n"
+        + "FMTFUr2SPscXk1k7muS+ZfEFwNPD4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT\n"
+        + "449CYoq8XBMBfvyWl/LLpw0r3JI6pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T\n"
+        + "8TKDGwwiuwiiT3SfkFSVdcjKulRuXSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iu\n"
+        + "RHSOuQENBFWdTP8BCADhhGxAA0pX5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnR\n"
+        + "tBScgKZnP0sjRTYEUIwmZuseHMBohtVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIe\n"
+        + "qCrm/6aejbFcQOpxe6U29KJRCAxuwNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZ\n"
+        + "oIvpIe9tZH4aXitCY2MCQH+hTyCyNBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+9\n"
+        + "7HCe042GIq65h0apgujyjhJidjch5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xP\n"
+        + "d9MncY5Q/eH+hn96694k5bckottSyGm/3f2Ihfj1ABEBAAGJASUEGAECAA8FAlWd\n"
+        + "TP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb1nsgRMgV\n"
+        + "YoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFyxo6lLHw9\n"
+        + "NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q3uwvP5fb\n"
+        + "fSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfqlOG7SPvM\n"
+        + "NmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk13ynADO+v\n"
+        + "EOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN9A==\n"
+        + "=1e/A\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "9A==\n"
+        + "=qbV3\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key that expired in 2006.
+   *
+   * <pre>
+   * pub   2048R/17DE1ACD 2005-07-08 [expired: 2006-07-08]
+   *       Key fingerprint = 1D9E EB79 DD38 B049 939D  9CAF 3CEC 781B 17DE 1ACD
+   * uid                  Testuser Three &lt;test3@example.com&gt;
+   * </pre>
+   */
+  public static final TestKey expiredKey() {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
+        + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
+        + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
+        + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
+        + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
+        + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAG0IlRlc3R1c2VyIFRocmVl\n"
+        + "IDx0ZXN0M0BleGFtcGxlLmNvbT6JAT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkI\n"
+        + "BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFC\n"
+        + "ECWLrcOeimuvwbmkonNzOkvKbGXl73GStISAksRWAHBQED1rEPC0NkFCDeVZO7df\n"
+        + "SYLlsqKwV6uSh05Ra0F5XeniC12YpAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCu\n"
+        + "R+8sNu/oecMRcFK4S9NaApi3vdqBNhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSk\n"
+        + "qcPfKZmocNXdgLV5Q80n3hc2y2nrl+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5\n"
+        + "btBW2L0UHtoEyiqkRfD6lX2laSLQmA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/\n"
+        + "2thO41K5AQ0EQs6nRQEIAM/833UHK1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3be\n"
+        + "eE4sh1NG5DbRCdo6iacZLarWr3FDz7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5F\n"
+        + "p5u2R4WF546bWqX45xPdLfHVTPyWB9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihw\n"
+        + "dxLsxaga+QmaL0bAR+dRcO6ucj7TDQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9Aj\n"
+        + "FoumMZ6l+k30sSdjSjpBMsNvPos0dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELp\n"
+        + "KgujZ2sKC9Nm395u6Q4cqUWihzb/Y7rIRuNHJarI7vUAEQEAAYkBJQQYAQIADwUC\n"
+        + "Qs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs7mvEWJI/\n"
+        + "1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Feyxb2rjtb\n"
+        + "NrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt10RaYR8VE\n"
+        + "ZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGdUt8U1Kq9\n"
+        + "OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/jPj5FUEU\n"
+        + "kE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6QHDJb\n"
+        + "=d/Xp\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "HDJb\n"
+        + "=RrXv\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A self-revoked key with no expiration.
+   *
+   * <pre>
+   * pub   2048R/7CA87821 2015-07-08 [revoked: 2015-07-08]
+   *       Key fingerprint = E328 CAB1 1F7E B1BC 1451  ABA5 0855 2A17 7CA8 7821
+   * uid                  Testuser Four &lt;test4@example.com&gt;
+   * </pre>
+   */
+  public static final TestKey selfRevokedKey() {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=477N\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=5aNq\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key with an additional user ID.
+   *
+   * <pre>
+   * pub   2048R/98C51DBF 2015-07-30
+   *       Key fingerprint = 42B3 294D 1924 D7EB AF4A  A99F 5024 BB44 98C5 1DBF
+   * uid                  foo:myId
+   * uid                  Testuser Five <test5@example.com>
+   * sub   2048R/C781A9E3 2015-07-30
+   * </pre>
+   */
+  public static TestKey validKeyWithSecondUserId() {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "54w16Vp/SVViJs7nTMSug0WREyd2\n"
+        + "=ldwB\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=uND5\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key revoked by a valid key, due to key compromise.
+   * <p>
+   * Revoked by {@link #validKeyWithoutExpiration()}.
+   *
+   * <pre>
+   * pub   2048R/3434B39F 2015-10-20 [revoked: 2015-10-20]
+   *       Key fingerprint = 931F 047D 7D01 DDEF 367A  8D90 8C4F D28E 3434 B39F
+   * uid                  Testuser Six &lt;test6@example.com&gt;
+   * </pre>
+   */
+  public static TestKey revokedCompromisedKey() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "tULvIEIjNNP8Qizfc4u2O8h7HP2b3yYSrp9MMQ==\n"
+        + "=Dxr7\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "i7Y7yHsc/ZvfJhKun0wx\n"
+        + "=M/kw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key revoked by a valid key, due to no longer being used.
+   * <p>
+   * Revoked by {@link #validKeyWithoutExpiration()}.
+   *
+   * <pre>
+   * pub   2048R/3D6C52D0 2015-10-20 [revoked: 2015-10-20]
+   *       Key fingerprint = 32DB 6C31 2ED7 A98D 11B2  43EA FAD2 ABE2 3D6C 52D0
+   * uid                  Testuser Seven &lt;test7@example.com&gt;
+   * </pre>
+   */
+  public static TestKey revokedNoLongerUsedKey() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "+B6paw/s8P7XCWznTBRdlFWWgrhcpzQ8fefC\n"
+        + "=CHer\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "bOdMFF2UVZaCuFynNDx958I=\n"
+        + "=aoJv\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * Key revoked by an expired key, after that key's expiration.
+   * <p>
+   * Revoked by {@link #expiredKey()}.
+   *
+   * <pre>
+   * pub   2048R/78BF7D7E 2005-08-01 [revoked: 2015-10-20]
+   *       Key fingerprint = 916F AB22 5BE7 7585 F59A  994C 001A DF8B 78BF 7D7E
+   * uid                  Testuser Eight &lt;test8@example.com&gt;
+   * </pre>
+   */
+  public static TestKey keyRevokedByExpiredKeyAfterExpiration() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "0d0PyzZT3676mQwmFoebMR9vACAeHjvDxD4F\n"
+        + "=ihWb\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "DCYWh5sxH28AIB4eO8PEPgU=\n"
+        + "=cSfw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * Key revoked by an expired key, before that key's expiration.
+   * <p>
+   * Revoked by {@link #expiredKey()}.
+   *
+   * <pre>
+   * pub   2048R/C43BF2E1 2005-08-01 [revoked: 2005-08-01]
+   *       Key fingerprint = 916D 6AD6 36A5 CBA6 B5A6  7274 6040 8661 C43B F2E1
+   * uid                  Testuser Nine &lt;test9@example.com&gt;
+   * </pre>
+   */
+  public static TestKey keyRevokedByExpiredKeyBeforeExpiration() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "REW68wsnWjwTtrPG8bqo6cCsOzqGYVt81eU=\n"
+        + "=FnZg\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "s8bxuqjpwKw7OoZhW3zV5Q==\n"
+        + "=JxsF\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestTrustKeys.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestTrustKeys.java
new file mode 100644
index 0000000..55bb9c2
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestTrustKeys.java
@@ -0,0 +1,1047 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.gpg.testutil;
+
+/**
+ * Test keys specific to web-of-trust checks.
+ * <p>
+ * In the following diagrams, the notation <code>M---N</code> indicates N trusts
+ * M, and an 'x' indicates the key is expired.
+ * <p>
+ *
+ * <pre>
+ *  A---Bx
+ *   \
+ *    \---C---D
+ *         \
+ *          \---Ex
+ *
+ *  D and E trust C to be a valid introducer of depth 2.
+ *
+ * F---G---F, in a cycle.
+ *
+ * H---I---J, but J is only trusted to length 1.
+ * </pre>
+ */
+public class TestTrustKeys {
+  /**
+   * pub   2048R/9FD0D396 2010-08-29
+   *       Key fingerprint = E401 17FC 4BF4 17BD 8F93  DEB1 D25A D07A 9FD0 D396
+   * uid                  Testuser A &lt;testa@example.com&gt;
+   * sub   2048R/F5C099DB 2010-08-29
+   */
+  public static TestKey keyA() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=DAMW\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=FLdD\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/B007D490 2010-08-29 [expired: 2011-08-29]
+   *       Key fingerprint = 355D 5B98 FECE 6199 83CD  C91D 5760 6987 B007 D490
+   * uid                  Testuser B &lt;testb@example.com&gt;
+   */
+  public static TestKey keyB() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6ntMBCADG7j/nuI+VvvNbPY9nnfLrAc3KTj0Z+DMxxUMYoZNLjTw1szQ0\n"
+        + "PuKKACiiSA9Oyj4R0aIhWdIR9iYxp6gQdje3yewzoqMwE+t5onYDpdX9QDFXyEzF\n"
+        + "UPWCjA7OSji1G6fyWakiYxKseqyRXOdHXI5TqMikBalmSpwwvmik0cfRGO+l6qvM\n"
+        + "mVJlcn6mkZB0d8WOPV8j8rFxmVSPn9SVP9L8HaFWv1uI9EY3zXbfNeDNgNeTWIMY\n"
+        + "75saINBA2LALBQ54u52GoSbaR8ukZYAjjkif3WIFI8B9xREwjUBLFy3E357aGyLZ\n"
+        + "jE8nsmPk4MDxDaeDNoSHJjcxtDWQJBub3u1zABEBAAG0HlRlc3R1c2VyIEIgPHRl\n"
+        + "c3RiQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqe0wIbAwUJAeEzgAYLCQgHAwIG\n"
+        + "FQgCCQoLBBYCAwECHgECF4AACgkQV2Bph7AH1JD0nQf/Vm+/Mvl99/y3Qw10S6et\n"
+        + "H6NYWDUeAKXe9mfXBJ39HdtlF50jZ5NzSwksAOSQtQZJ3tQQeElXB29cZDvAscva\n"
+        + "RiTtt+KUxDZSYbEHrC0EO7w0Wi5ltwaWdXnoitMOgPZ/grL7UpUbL8rB1evfLbhm\n"
+        + "AqC/6kgHuXeY/7EAzwU3o0wKbmfx1sh8AyQSi4unUwIDCV1RIAP0+ZfJSg0WwGoS\n"
+        + "JB5+lKajtIE6kMn9m8CWM66/zxSCY3XLcoXvjVxCYPwwgYSyje8dDxxOI+x7uj2I\n"
+        + "IjM5RHQ9hTsR7NQ9JUTFmpKZlcdah93NZLKJAFLUtOPjMa5d5t2O0ZOxZ5ftlhHp\n"
+        + "Q7kBDQRMep7TAQgAwOuLBXnACIsd879ld/vLcn8umpKV8MIUjrqOMjR0rNKpCUDw\n"
+        + "LxL4uVh3q/ksESHnQPPqxFYkgeA66SYrx4jwZjbZ5vv9BW99LHe8lSahqrJA9A9g\n"
+        + "5iw5hH+2ZWrGlu3P65UdQUJW+JaDx1IIBt3BbmdGDuKF/ESsy9qxEKq7tKqHI2JL\n"
+        + "Ed+6OIwWblU7ZogfiNpgZJ0lapxTe84mGsD0TowGTu5re/8wIJf1f2q4PuG+L9OZ\n"
+        + "0ZD5i9s1MAxdw4OD+705owPCQnqsr18nH9aUBHWJn9NCXb3jL7QGaId84Yq8SRlK\n"
+        + "wHSRtHLLJoowJ5fXw5UbZcUtRUergxFRwae87wARAQABiQElBBgBAgAPBQJMep7T\n"
+        + "AhsMBQkB4TOAAAoJEFdgaYewB9SQMbsH/iu1HY7OMJxd8EkfxairRNec/v9uEvYQ\n"
+        + "XqfEPw/Hihdef1TY8vB69ymAPd89e1PRDj1m+0/RivO045qFP7lbWMkjKeR9dXXe\n"
+        + "UzIEsTUJ1CNnA7C3fo11NBVg59E0d84bMKQx7n4AZkljgKFKghUb6OJZiWRdh+8W\n"
+        + "0I95JI2R7nMYw3L8/sSGxt+Vjhs9acB1DldbyYbJitYA4fhVZQH9zgeuhQqdCULQ\n"
+        + "ZexpkQqvG0o4iJKO4yeJNHdeM+NwH38wXfzydtEv6Dxz/YZSTwt08p97l6DQ//H7\n"
+        + "wek1LcqeX47YFa9Ftns8Y8fjh4S8Kyi1F6BhZKbsdDqg2hA+0AFv7LA=\n"
+        + "=tmW1\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=uFLT\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/D24FE467 2010-08-29
+   *       Key fingerprint = 6C21 10AC F4FC 1C7B F270  C00E 641F 1193 D24F E467
+   * uid                  Testuser C &lt;testc@example.com&gt;
+   * sub   2048R/DBECD4FA 2010-08-29
+   */
+  public static TestKey keyC() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=LtMR\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=5pIh\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/0FDD3677 2010-08-29
+   *       Key fingerprint = C96C 5E9D 669C 448A D1B9  BEB5 A991 E2D5 0FDD 3677
+   * uid                  Testuser D &lt;testd@example.com&gt;
+   * sub   2048R/CAB81AE0 2010-08-29
+   */
+  public static TestKey keyD() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6nwkBCADuztv2tGhjPljwW46qEhth7ZnkdhYXuctZ6lNQuy5LMaEECE3C\n"
+        + "jvVKY+nBrgsLY2Trts+q+mdooBWvxy/qe5PAQTcPR83KjVS4fYwNMBgeRxBEZAZg\n"
+        + "DFwRRCsRrHost+cMgtzLocQ+vL3+9yTRAIe/WmYwbEDXg/c9JSC7kQbZqaAaOshO\n"
+        + "cIOyeB8/QoYee0fEnBzHMmcd0SB1YpwIvRG6v61lXmgpQ9CbovvXO6ZZyEyCX784\n"
+        + "9xprzqP1y03DPrbhuhBAY8EMf3KGJA1dEcU4+lbGEgmlOe2YSbWoLs7mRLFcq5xx\n"
+        + "JroYMtvXF04k4ZHNZAnT3IZc+lJyCqOp4vXpABEBAAG0HlRlc3R1c2VyIEQgPHRl\n"
+        + "c3RkQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqfCQIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQqZHi1Q/dNne/0wgApuPzh4J8p2quCK1ScsJHlgGRojGq\n"
+        + "IDPhZFtPn0p2IAkqr5sAhvZAjd3u9A2DqQ7pwOX7gnGRE7dSrK69IAjfbRMc5k16\n"
+        + "aBK2ADq2YgPEmTToots1A0Tj+LaCFOXYUtEkgAC+RfFIkCdt8z86GIr0kg19Q/vY\n"
+        + "I/LtvThAk28D8yIfDnW49Mc4GGq+qvrOytBaGu3dzW0mjYWGEyl0fdSjNqtKyWN7\n"
+        + "Qw70Kqysaoy1KiPRAgwiPQfMCEx6pVaXuAfgRKaJ18kCNOldpajLgQv6yeY7mhgu\n"
+        + "Q3Qe7xQlAtVObxskcTH2CWggl2dPqSMNieLK0g/ER8PIReGDCBXNSJ4qYbkBDQRM\n"
+        + "ep8JAQgAw/o1nhJPLGlIfEMzOGU0Jjj+DwEyB3QIEEc+WKRvgtGsJ4cbZdaGWBJq\n"
+        + "jSo7e9XC9jA2ih0+Gld0vWV7S0LZ84xXxQeadC+AZBFR+b9ga4aUFIji8Tdi2dWX\n"
+        + "QmY76hHIaF8rs6aJB7lRig735VRLxVHOb194t9KLUzZiEKqd71BvLQyuLqAfTEsT\n"
+        + "GRHgmydaxZbGXz+Z57jbQgm11CQEHX1dtS8uqWb64xrV5GAeuEhRj4R6Yiy7OPNi\n"
+        + "xXHxryH2Jd34pA0cGHYVcTgVjXuZ9FFP2SnXuxABONGAIaJuqg7ozYBa2kOdr0DN\n"
+        + "5Pxy5ocR7R2ZoN0pYD5+Cc7oGHjuCQARAQABiQEfBBgBAgAJBQJMep8JAhsMAAoJ\n"
+        + "EKmR4tUP3TZ369QIAKPlfX2TUfhP3otYiaa24zBJ/cvGljGiSfX0KrausBHH161j\n"
+        + "lraJfLzpe7vSOZhwZwgIY/eKoErAkJwVnX1+dLuOcHaqRDi5gnLqa6Yg9a2LWb4z\n"
+        + "rvgsvbiNUs1o9htOcvcpv7e3UUUcRa8lO+aNkO+VoI6DI8RJ3wIfJayboePRXdfr\n"
+        + "8g9of0jSdIOzlaaBPxA2wYSWXm4kv7QXzZooxuGqhn0+JKuq2+oO9y5QUig+c3oG\n"
+        + "a5mpVblmv5ZL6Gc36kCbeEC8j6JkNT4wnceQwpNUNYtPU186cjy3rAD4C58w0Uvp\n"
+        + "HZZSTc0syLOShQr//We39LUNaX6WF3NmyF8K/OM=\n"
+        + "=YDhQ\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=e1xT\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/4B4387EE 2010-08-29 [expired: 2011-08-29]
+   *       Key fingerprint = F01D 677C 8BDB 854E 1054  406E 3B09 B97F 4B43 87EE
+   * uid                  Testuser E &lt;teste@example.com&gt;
+   */
+  public static TestKey keyE() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6nxoBCADjYOWOFa7ZBJpRuNspRoXBTK0LiK5zqN894b87LgIYEgUM6q5J\n"
+        + "yLNo43x7V+ow1/7BEq0JUAMSQ3uRn2jqXiJskSXvwlFYcTVFb0gY09CSD0ptHvda\n"
+        + "zqYOuM/MU1l9jqmlM+pDw/z0pLTKYmAHi6pKJ64pqccMHPUZHpLywyzSNX+JM86I\n"
+        + "K5KAsyGArtgpT9vfci3idNeXjhMR8rfLPDFbdGvGFOZrYv0cfgTbBpVEWeHjs2FR\n"
+        + "4vHG133AdjdZcvA9Y9VW34ZLeiyBEeFix7+HPVS82rko2kQxZu1UZRu340maKDAo\n"
+        + "+UVirgo0FQ8nNUR+c9oNKgiZtO39IAPJv/WZABEBAAG0HlRlc3R1c2VyIEUgPHRl\n"
+        + "c3RlQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqfGgIbAwUJAeEzgAYLCQgHAwIG\n"
+        + "FQgCCQoLBBYCAwECHgECF4AACgkQOwm5f0tDh+6Fowf9FZgntlW4qc7BHe8zYJ0q\n"
+        + "zoLZrHwCFcaeO3kz53y5Lz3+plMuqVDjoQDOt8DxsPHrXWKiu0qBTjZ28ztN3ef6\n"
+        + "f0MpguTGclvFroevUct0xiyox5r1DfMT8JRvqsojE1XPscR2zJzIgEg3OCPuksT9\n"
+        + "EsHsF+/3RBbsXbQgDpW38g0GzIJI4AiQ/yvG2ON9awN2kzIWoBkthVCGy54lCTGj\n"
+        + "yPhatE7Zu2ABNcerIDstupWww2Psec6pGbPPci8ojc90fzalk3UMXcXHD7m8cTJS\n"
+        + "kgHScOzTElIQqOA1+w6uiHy2oAn+qW7534j6p9Tj+DrSIzUXBedGjXZevaKaurVy\n"
+        + "KLkBDQRMep8aAQgAn5r6toYnEzwDeig8r+t89vqOFtohYcmtyeLeTiTTdAz/xWBW\n"
+        + "HUlqV8sglQ9aINpGtBf37v13RhtU3WkUv8cZMQoRM8P2H3cKDNwkucFO6uKSEQO5\n"
+        + "FdzTm4C4WaoE7QiTRbiekwh7O54mz4Wup6LHuEFQEcSpdRUp8w/qaJIHG9EJad1q\n"
+        + "UEsKNnITW+mWHY3+ccK1hgqPwOPqO3/8QtaipekKOYAtOb+57c1jtDFBZnYIkant\n"
+        + "oKs+kRw0DykXFTyFOMYqaleBMcVG+u7ljwAq18L8Ev+qVIpBIZ5eQ5+6p1w9B69h\n"
+        + "RH0Ebn50ebpoqKOXhN4/bu/wq596y0o4xDB0GQARAQABiQElBBgBAgAPBQJMep8a\n"
+        + "AhsMBQkB4TOAAAoJEDsJuX9LQ4fu0/wH/35/22xina8ktbvGV/kB0pH2LBqeXN/b\n"
+        + "CLdA+CDzfwMDzqG0kU39EJ3Fbux7fj4uMaeiYfbO9U85+NOuDmeH41B2dM9S1AzE\n"
+        + "H+/OiCp/Zf1fdd1qXhsA4Xe5vc/VD9oso9OrZK5CM5u0TPmYFijfVDPNgag6mPnD\n"
+        + "zd8JCsuEj4VEy6NF1KcoCc8edQ8AZ4L6ZQ6qiV24gxLnh8xImVr5YjBKDUCdrl79\n"
+        + "0u4wekfgapSx9Sw9Ycz5dFOL07OOHPiKZwUG0f8td6oJX4Ddxset5JAm1pPcLQHR\n"
+        + "6PRx0hI/Tz7rsAI6O37/BEM15+MVGIgOSLL/SRIpOa0L8qmuUhhS6Bg=\n"
+        + "=uA5x\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=HTKj\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/31FA48C4 2010-09-01
+   *       Key fingerprint = 85CE F045 8113 42DA 14A4  42AA 4A9F AC70 31FA 48C4
+   * uid                  Testuser F &lt;testf@example.com&gt;
+   * sub   2048R/50FF7D5C 2010-09-01
+   */
+  public static TestKey keyF() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=WDx2\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=ZLpl\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/E2D32BA5 2010-09-01
+   *       Key fingerprint = CB2B 665B 88DA D56A 7009  C15D 8A9B 5293 E2D3 2BA5
+   * uid                  Testuser G &lt;testg@example.com&gt;
+   * sub   2048R/829DAE8D 2010-09-01
+   */
+  public static TestKey keyG() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=34GE\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=T1WV\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/080E5723 2010-09-01
+   *       Key fingerprint = 2957 ABE4 937D A84A 2E5D  31DB 65C4 33C4 080E 5723
+   * uid                  Testuser H &lt;testh@example.com&gt;
+   * sub   2048R/68C7C262 2010-09-01
+   */
+  public static TestKey keyH() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=lddL\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=voB9\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/5E9AEDD0 2010-09-01
+   *       Key fingerprint = 818D 5D0B 4AE2 A4FE A4C3  C44D D022 DA3A 5E9A EDD0
+   * uid                  Testuser I &lt;testi@example.com&gt;
+   * sub   2048R/0884E452 2010-09-01
+   */
+  public static TestKey keyI() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=SiG3\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=RcWw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/C2E6A198 2010-09-01
+   *       Key fingerprint = 83AB CE4D 6845 D6DA F7FB  AA47 A139 3C46 C2E6 A198
+   * uid                  Testuser J &lt;testj@example.com&gt;
+   * sub   2048R/863E8ABF 2010-09-01
+   */
+  public static TestKey keyJ() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx+aUgBCADczNio9UWnUggUZkdqJye57497oD9vNo9rmtR+i1TkMpVeWaMH\n"
+        + "UWrm1twzIPV9D4lAWLJoG2cYF6nXG1JxsKc9mOIZ6O1WfsopMUU0p+EfU8H/cvdM\n"
+        + "/iccYS6OnNL4/xR1R7hlA4+b/jaOZfzdS3i5jwOf+TtCk7c5qOuFhraiVQ9H1G86\n"
+        + "+LsiWVeXEFc/FXxnESRmbaZFNJrAdJl23eKXRC6az0S5FwMVBvUhRpLwIGDbVT/0\n"
+        + "/QwaNUOq3bYwudPREFLg/1HtBuxNhRdV6mCrit+tsPan9o0/WtsHuq8n4/pqOKBc\n"
+        + "RRmOIQR9SEohE2TuVT3XVFpMXa4a4CBuNXjTABEBAAG0HlRlc3R1c2VyIEogPHRl\n"
+        + "c3RqQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pSAIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQoTk8RsLmoZi0BggAlnbCwmwaLwcpU9YcOE9/8KF56dIs\n"
+        + "XhdxzqdP91UmhVT0df1OBhgTqFkKprBLCT+B9yBClsnyXMatkvuhQG6C7lw9toMO\n"
+        + "TITRPZoFJe3Ezi+HRRPqAPubIcSgeILuilvFhkoUOgoC1ubmVPgcGBLb8tdvI3bA\n"
+        + "svq+n2jaYUlgL5N6ZNRNakc07e8vH5SeKiD8ZntJlTU49fkxzlawtDaI3+GhyUiB\n"
+        + "0Ah8pl143DFNAq8CfvQCPKwX4WFPkEflh0LlgaEPJUZ/H6KxKXXF8SC9cD2VIii8\n"
+        + "Yrue8y9T+j5y699A0GCptb1IKrgxbfhgD//3g3l1eXsEwn2cwFNCt7pZFLkBDQRM\n"
+        + "fmlIAQgA3E2pM6oDJGgfxbqSfykuRtTbiAi7JEd1DNvEe6gJ7qkBLM4ipILBD0qR\n"
+        + "qCwL37E4/3nMsZjA7GIFLQj2DrFW3aEEKwR/zdh7R67lo9CunCY+FPWTuOkCG8Sh\n"
+        + "3RLpbAV6I61NG/wDznW30vmKNJDgPpkzYj8u0T4MtpywEgxTxCqWZKCufWDRfNAy\n"
+        + "IBLt+piG+bcYKfw9pS8PvXPQMNIi4U2pu3hb/BHC3Y1A8FVpEe4CFV7rWb/K2Ydx\n"
+        + "eBxwwxm9sBxF+vhlI+ZEeb9JxGH6jYlc6twD4e6p3KqusAKLKiLsS5uLQnpMGGZ8\n"
+        + "vcpTSfyHjG2QHc3qG9S/yDCZjhhe2QARAQABiQEfBBgBAgAJBQJMfmlIAhsMAAoJ\n"
+        + "EKE5PEbC5qGYClMIANTdZ+/g/FPl1Lm0tO1CSnHVHekeGNA9n3L6SGiSZQJjEDo0\n"
+        + "gsye5xgxh5JGKf7CqbEFfeLC9Jx5W5EN4dVFudncIlC/SutfRzdt5W8CLXMl0c41\n"
+        + "5FmtpWNStk3MglkcjE5PrRRrSiRc45S0e2kIPb8eiVKg98/rCToC9+Qn3pMi/fcM\n"
+        + "LVpYE+dhvB5EhOSwBWWgvWXzeLhv5CnBKxH0ItHhNwvt8qPOHgQAJKPc6dV888xn\n"
+        + "Sew62LFefHPnGTHLP8RRgVIvZBG5IoovxSz89QGHQZiC4xv00I7zNwmtr6eEcl+y\n"
+        + "BkUK9QWITEBHUDqR+cbVa2dRr3fUHwRP7G2G+ow=\n"
+        + "=ucAX\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "=NiQI\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  private TestTrustKeys() {
+  }
+}
diff --git a/gerrit-gwtdebug/BUCK b/gerrit-gwtdebug/BUCK
index bf05af0..3670916 100644
--- a/gerrit-gwtdebug/BUCK
+++ b/gerrit-gwtdebug/BUCK
@@ -2,6 +2,7 @@
   name = 'gwtdebug',
   srcs = glob(['src/main/java/**/*.java']),
   deps = [
+    '//gerrit-pgm:daemon',
     '//gerrit-pgm:pgm',
     '//gerrit-pgm:util',
     '//gerrit-util-cli:cli',
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
index 5da8b1e..4b6e7e4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -274,6 +274,7 @@
     try {
       t.setText(getText());
       content.add(t);
+      t.setFocus(true);
       t.selectAll();
 
       boolean ok = execCommand("copy");
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index 2a79db4..ef4de82 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -64,6 +64,7 @@
   deps = [
     ':client',
     '//lib:junit',
+    '//lib/gwt:user',
     '//lib/jgit:jgit',
   ],
   source_under_test = [':client'],
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 0db7ea4..a5a02cd 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -104,4 +104,7 @@
 
   @Source("warning.png")
   public ImageResource warning();
+
+  @Source("question.png")
+  public ImageResource question();
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 695c126..dd88b04 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -296,6 +296,7 @@
     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 AccountInfo uploader() /*-{ return this.uploader; }-*/;
     public final native boolean isEdit() /*-{ return this._number == 0; }-*/;
     public final native CommitInfo commit() /*-{ return this.commit; }-*/;
     public final native void setCommit(CommitInfo c) /*-{ this.commit = c; }-*/;
@@ -310,6 +311,9 @@
     public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
 
+    public final native boolean hasPushCertificate() /*-{ return this.hasOwnProperty('push_certificate'); }-*/;
+    public final native PushCertificateInfo pushCertificate() /*-{ return this.push_certificate; }-*/;
+
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
       final int editParent = findEditParent(list);
       Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
index b21078e..d95f9ef 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
@@ -30,6 +30,15 @@
   public final native boolean binary() /*-{ return this.binary || false; }-*/;
   public final native String status() /*-{ return this.status; }-*/;
 
+
+  // JSNI methods cannot have 'long' as a parameter type or a return type and
+  // it's suggested to use double in this case:
+  // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html#important
+  public final long sizeDelta() {
+    return (long)_sizeDelta();
+  }
+  private final native double _sizeDelta() /*-{ return this.size_delta || 0; }-*/;
+
   public final native int _row() /*-{ return this._row }-*/;
   public final native void _row(int r) /*-{ this._row = r }-*/;
 
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
index f0f3b66..55ef892 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
@@ -37,6 +37,7 @@
   public final native String allProjects() /*-{ return this.all_projects; }-*/;
   public final native String allUsers() /*-{ return this.all_users; }-*/;
   public final native String docUrl() /*-{ return this.doc_url; }-*/;
+  public final native boolean editGpgKeys() /*-{ return this.edit_gpg_keys || false; }-*/;
   public final native String reportBugUrl() /*-{ return this.report_bug_url; }-*/;
   public final native String reportBugText() /*-{ return this.report_bug_text; }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
similarity index 68%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
index d1bb426..f7477a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
@@ -12,17 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.account;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArrayString;
 
 public class GpgKeyInfo extends JavaScriptObject {
+  public enum Status {
+    BAD, OK, TRUSTED;
+  }
+
   public final native String id() /*-{ return this.id; }-*/;
   public final native String fingerprint() /*-{ return this.fingerprint; }-*/;
   public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/;
   public final native String key() /*-{ return this.key; }-*/;
 
+  private final native String statusRaw() /*-{ return this.status; }-*/;
+  public final Status status() {
+    String s = statusRaw();
+    if (s == null) {
+      return null;
+    }
+    return Status.valueOf(s);
+  }
+
+  public final native boolean hasProblems()
+  /*-{ return this.hasOwnProperty('problems'); }-*/;
+  public final native JsArrayString problems() /*-{ return this.problems; }-*/;
+
   protected GpgKeyInfo() {
   }
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java
new file mode 100644
index 0000000..ebfec1a
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.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.client.info;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class PushCertificateInfo extends JavaScriptObject {
+  public final native String certificate() /*-{ return this.certificate; }-*/;
+  public final native GpgKeyInfo key() /*-{ return this.key; }-*/;
+
+  protected PushCertificateInfo() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java
new file mode 100644
index 0000000..10e20bf
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+
+/** A {@code Suggestion} with highlights. */
+public class HighlightSuggestion implements Suggestion {
+
+  private final String keyword;
+  private final String value;
+
+  public HighlightSuggestion(String keyword, String value) {
+    this.keyword = keyword;
+    this.value = value;
+  }
+
+  @Override
+  public String getDisplayString() {
+    int start = 0;
+    int keyLen = keyword.length();
+    SafeHtmlBuilder builder = new SafeHtmlBuilder();
+    for (;;) {
+      int index = value.indexOf(keyword, start);
+      if (index == -1) {
+        builder.appendEscaped(value.substring(start));
+        break;
+      }
+      builder.appendEscaped(value.substring(start, index));
+      builder.appendHtmlConstant("<strong>");
+      start = index + keyLen;
+      builder.appendEscaped(value.substring(index, start));
+      builder.appendHtmlConstant("</strong>");
+    }
+    return builder.toSafeHtml().asString();
+  }
+
+  @Override
+  public String getReplacementString() {
+    return value;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
new file mode 100644
index 0000000..f25fc3f
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java
new file mode 100644
index 0000000..44ed50b
--- /dev/null
+++ b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class HighlightSuggestionTest {
+
+  @Test
+  public void singleHighlight() throws Exception {
+    String keyword = "key";
+    String value = "somethingkeysomething";
+    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
+    assertEquals(
+        "something<strong>key</strong>something",
+        suggestion.getDisplayString());
+    assertEquals(value, suggestion.getReplacementString());
+  }
+
+  @Test
+  public void noHighlight() throws Exception {
+    String keyword = "key";
+    String value = "something";
+    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
+    assertEquals(value, suggestion.getDisplayString());
+    assertEquals(value, suggestion.getReplacementString());
+  }
+
+  @Test
+  public void doubleHighlight() throws Exception {
+    String keyword = "key";
+    String value = "somethingkeysomethingkeysomething";
+    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
+    assertEquals(
+        "something<strong>key</strong>something<strong>key</strong>something",
+        suggestion.getDisplayString());
+    assertEquals(value, suggestion.getReplacementString());
+  }
+}
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
index 90fdcab..ead19f4 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -1,7 +1,7 @@
 include_defs('//gerrit-gwtui/gwt.defs')
 include_defs('//tools/gwt-constants.defs')
 
-DEPS = [
+DEPS = GWT_TRANSITIVE_DEPS + [
   '//gerrit-gwtexpui:CSS',
   '//lib:gwtjsonrpc',
   '//lib/gwt:dev',
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 946888d..ba3cc4c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -28,6 +28,8 @@
 import static com.google.gerrit.common.PageLinks.SETTINGS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_CONTACT;
+import static com.google.gerrit.common.PageLinks.SETTINGS_DIFF_PREFERENCES;
+import static com.google.gerrit.common.PageLinks.SETTINGS_EDIT_PREFERENCES;
 import static com.google.gerrit.common.PageLinks.SETTINGS_EXTENSION;
 import static com.google.gerrit.common.PageLinks.SETTINGS_GPGKEYS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD;
@@ -41,6 +43,8 @@
 
 import com.google.gerrit.client.account.MyAgreementsScreen;
 import com.google.gerrit.client.account.MyContactInformationScreen;
+import com.google.gerrit.client.account.MyDiffPreferencesScreen;
+import com.google.gerrit.client.account.MyEditPreferencesScreen;
 import com.google.gerrit.client.account.MyGpgKeysScreen;
 import com.google.gerrit.client.account.MyGroupsScreen;
 import com.google.gerrit.client.account.MyIdentitiesScreen;
@@ -66,6 +70,7 @@
 import com.google.gerrit.client.admin.ProjectInfoScreen;
 import com.google.gerrit.client.admin.ProjectListScreen;
 import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.admin.ProjectTagsScreen;
 import com.google.gerrit.client.api.ExtensionScreen;
 import com.google.gerrit.client.api.ExtensionSettingsScreen;
 import com.google.gerrit.client.change.ChangeScreen;
@@ -526,6 +531,14 @@
           return new MyPreferencesScreen();
         }
 
+        if (matchExact(SETTINGS_DIFF_PREFERENCES, token)) {
+          return new MyDiffPreferencesScreen();
+        }
+
+        if (matchExact(SETTINGS_EDIT_PREFERENCES, token)) {
+          return new MyEditPreferencesScreen();
+        }
+
         if (matchExact(SETTINGS_PROJECTS, token)) {
           return new MyWatchedProjectsScreen();
         }
@@ -538,7 +551,8 @@
           return new MySshKeysScreen();
         }
 
-        if (matchExact(SETTINGS_GPGKEYS, token)) {
+        if (matchExact(SETTINGS_GPGKEYS, token)
+            && Gerrit.info().gerrit().editGpgKeys()) {
           return new MyGpgKeysScreen();
         }
 
@@ -733,11 +747,16 @@
             return new ProjectInfoScreen(k);
           }
 
-          if (ProjectScreen.BRANCH.equals(panel)
-              || matchPrefix(ProjectScreen.BRANCH, panel)) {
+          if (ProjectScreen.BRANCHES.equals(panel)
+              || matchPrefix(ProjectScreen.BRANCHES, panel)) {
             return new ProjectBranchesScreen(k);
           }
 
+          if (ProjectScreen.TAGS.equals(panel)
+              || matchPrefix(ProjectScreen.TAGS, panel)) {
+            return new ProjectTagsScreen(k);
+          }
+
           if (ProjectScreen.ACCESS.equals(panel)) {
             return new ProjectAccessScreen(k);
           }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 80aa9cc..c77b71f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.info.AccountPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gwt.i18n.client.NumberFormat;
 
 import java.util.Date;
 
@@ -107,4 +108,30 @@
   private static AccountFormatter createAccountFormatter() {
     return new AccountFormatter(Gerrit.info().user().anonymousCowardName());
   }
+
+  /** The returned format string doesn't contain any +/- sign. */
+  public static String formatAbsBytes(long bytes) {
+    return formatBytes(bytes, true);
+  }
+
+  public static String formatBytes(long bytes) {
+    return formatBytes(bytes, false);
+  }
+
+  private static String formatBytes(long bytes, boolean abs) {
+    bytes = abs ? Math.abs(bytes) : bytes;
+
+    if (bytes == 0) {
+      return abs ? "0 B" : "+/- 0 B";
+    }
+
+    if (Math.abs(bytes) < 1024) {
+      return (bytes > 0 && !abs ? "+" : "") + bytes + " B";
+    }
+
+    int exp = (int) (Math.log(Math.abs(bytes)) / Math.log(1024));
+    return (bytes > 0 && !abs ? "+" : "")
+        + NumberFormat.getFormat("#.0").format(bytes / Math.pow(1024, exp))
+        + " " + "KMGTPE".charAt(exp - 1) + "iB";
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index d52f2fc..cd53b8e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -20,6 +20,7 @@
 
 import com.google.gerrit.client.account.AccountApi;
 import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.account.EditPreferences;
 import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.api.PluginLoader;
@@ -46,6 +47,7 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.common.data.SystemInfoService;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GerritTopMenu;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Project;
@@ -118,6 +120,7 @@
   private static HostPageData.Theme myTheme;
   private static String defaultScreenToken;
   private static AccountDiffPreference myAccountDiffPref;
+  private static EditPreferencesInfo editPrefs;
   private static String xGerritAuth;
   private static boolean isNoteDbEnabled;
 
@@ -327,6 +330,15 @@
     myAccountDiffPref = accountDiffPref;
   }
 
+  /** @return the edit preferences of the current user, null if not signed-in */
+  public static EditPreferencesInfo getEditPreferences() {
+    return editPrefs;
+  }
+
+  public static void setEditPreferences(EditPreferencesInfo p) {
+    editPrefs = p;
+  }
+
   /** @return true if the user is currently authenticated */
   public static boolean isSignedIn() {
     return xGerritAuth != null;
@@ -390,6 +402,7 @@
   static void deleteSessionCookie() {
     myAccount = AccountInfo.create(0, null, null, null);
     myAccountDiffPref = null;
+    editPrefs = null;
     myPrefs = AccountPreferencesInfo.createDefault();
     urlAliasMatcher.clearUserAliases();
     xGerritAuth = null;
@@ -478,16 +491,26 @@
                 }
           }));
           AccountApi.self().view("preferences")
-              .get(cbg.addFinal(new GerritCallback<AccountPreferencesInfo>() {
+              .get(cbg.add(new GerritCallback<AccountPreferencesInfo>() {
             @Override
             public void onSuccess(AccountPreferencesInfo prefs) {
               myPrefs = prefs;
               onModuleLoad2(result);
             }
           }));
+          AccountApi.getEditPreferences(
+              cbg.addFinal(new GerritCallback<EditPreferences>() {
+            @Override
+            public void onSuccess(EditPreferences prefs) {
+              EditPreferencesInfo prefsInfo = new EditPreferencesInfo();
+              prefs.copyTo(prefsInfo);
+              editPrefs = prefsInfo;
+            }
+          }));
         } else {
           myAccount = AccountInfo.create(0, null, null, null);
           myPrefs = AccountPreferencesInfo.createDefault();
+          editPrefs = null;
           onModuleLoad2(result);
         }
       }
@@ -697,7 +720,8 @@
     menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
     projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsInfo(), ProjectScreen.INFO));
-    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsBranches(), ProjectScreen.BRANCH));
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsBranches(), ProjectScreen.BRANCHES));
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsTags(), ProjectScreen.TAGS));
     projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsAccess(), ProjectScreen.ACCESS));
     final LinkMenuItem dashboardsMenuItem =
         new ProjectLinkMenuItem(C.menuProjectsDashboards(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 6bbc8f1..269999c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -77,6 +77,7 @@
   String menuProjectsList();
   String menuProjectsInfo();
   String menuProjectsBranches();
+  String menuProjectsTags();
   String menuProjectsAccess();
   String menuProjectsDashboards();
   String menuProjectsCreate();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 05de983..fb74506 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -60,6 +60,7 @@
 menuProjectsList = List
 menuProjectsInfo = General
 menuProjectsBranches = Branches
+menuProjectsTags = Tags
 menuProjectsAccess = Access
 menuProjectsDashboards = Dashboards
 menuProjectsCreate = Create New Project
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index f010879..d7f878f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -29,6 +29,7 @@
   String addWatchPanel();
   String avatarInfoPanel();
   String bottomheader();
+  String branchTableDeleteButton();
   String branchTablePrevNextLinks();
   String cAPPROVAL();
   String cLastUpdate();
@@ -141,6 +142,7 @@
   String needsReview();
   String negscore();
   String noborder();
+  String pagingLink();
   String patchBrowserPopup();
   String patchBrowserPopupBody();
   String patchCellReverseDiff();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index 85ebeb7..a1bcfe8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
@@ -37,6 +38,17 @@
     return new RestApi("/accounts/").view("self");
   }
 
+  /** Retrieve the account edit preferences */
+  public static void getEditPreferences(AsyncCallback<EditPreferences> cb) {
+    self().view("preferences.edit").get(cb);
+  }
+
+  /** Put the account edit preferences */
+  public static void putEditPreferences(EditPreferences in,
+      AsyncCallback<VoidResult> cb) {
+    self().view("preferences.edit").put(in, cb);
+  }
+
   public static void suggest(String query, int limit,
       AsyncCallback<JsArray<AccountInfo>> cb) {
     new RestApi("/accounts/")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 1bf4034..94884fa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -52,6 +52,8 @@
   String tabAccountSummary();
   String tabAgreements();
   String tabContactInformation();
+  String tabDiffPreferences();
+  String tabEditPreferences();
   String tabGpgKeys();
   String tabHttpAccess();
   String tabMyGroups();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 6d91807..4580aea 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -38,6 +38,8 @@
 tabAccountSummary = Profile
 tabAgreements = Agreements
 tabContactInformation = Contact Information
+tabDiffPreferences = Diff Preferences
+tabEditPreferences = Edit Preferences
 tabGpgKeys = GPG Public Keys
 tabHttpAccess = HTTP Password
 tabMyGroups = Groups
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
new file mode 100644
index 0000000..6e4b1c7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class EditPreferences extends JavaScriptObject {
+  public static EditPreferences create(EditPreferencesInfo in) {
+    EditPreferences p = createObject().cast();
+    p.tabSize(in.tabSize);
+    p.lineLength(in.lineLength);
+    p.cursorBlinkRate(in.cursorBlinkRate);
+    p.hideTopMenu(in.hideTopMenu);
+    p.showTabs(in.showTabs);
+    p.showWhitespaceErrors(in.showWhitespaceErrors);
+    p.syntaxHighlighting(in.syntaxHighlighting);
+    p.hideLineNumbers(in.hideLineNumbers);
+    p.matchBrackets(in.matchBrackets);
+    p.autoCloseBrackets(in.autoCloseBrackets);
+    p.theme(in.theme);
+    p.keyMapType(in.keyMapType);
+    return p;
+  }
+
+  public final void copyTo(EditPreferencesInfo p) {
+    p.tabSize = tabSize();
+    p.lineLength = lineLength();
+    p.cursorBlinkRate = cursorBlinkRate();
+    p.hideTopMenu = hideTopMenu();
+    p.showTabs = showTabs();
+    p.showWhitespaceErrors = showWhitespaceErrors();
+    p.syntaxHighlighting = syntaxHighlighting();
+    p.hideLineNumbers = hideLineNumbers();
+    p.matchBrackets = matchBrackets();
+    p.autoCloseBrackets = autoCloseBrackets();
+    p.theme = theme();
+    p.keyMapType = keyMapType();
+  }
+
+  public final void theme(Theme i) {
+    setThemeRaw(i != null ? i.toString() : Theme.DEFAULT.toString());
+  }
+  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+
+  public final void keyMapType(KeyMapType i) {
+    setkeyMapTypeRaw(i != null ? i.toString() : KeyMapType.DEFAULT.toString());
+  }
+  private final native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
+
+  public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
+  public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
+  public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
+  public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
+  public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
+  public final native void showWhitespaceErrors(boolean s) /*-{ this.show_whitespace_errors = s }-*/;
+  public final native void syntaxHighlighting(boolean s) /*-{ this.syntax_highlighting = s }-*/;
+  public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
+  public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
+  public final native void autoCloseBrackets(boolean c) /*-{ this.auto_close_brackets = c }-*/;
+
+  public final Theme theme() {
+    String s = themeRaw();
+    return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
+  }
+  private final native String themeRaw() /*-{ return this.theme }-*/;
+
+  public final KeyMapType keyMapType() {
+    String s = keyMapTypeRaw();
+    return s != null ? KeyMapType.valueOf(s) : KeyMapType.DEFAULT;
+  }
+  private final native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
+
+  public final int tabSize() {
+    return get("tab_size", 8);
+  }
+
+  public final int lineLength() {
+    return get("line_length", 100);
+  }
+
+  public final int cursorBlinkRate() {
+    return get("cursor_blink_rate", 0);
+  }
+
+  public final native boolean hideTopMenu() /*-{ return this.hide_top_menu || false }-*/;
+  public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
+  public final native boolean showWhitespaceErrors() /*-{ return this.show_whitespace_errors || false }-*/;
+  public final native boolean syntaxHighlighting() /*-{ return this.syntax_highlighting || false }-*/;
+  public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
+  public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
+  public final native boolean autoCloseBrackets() /*-{ return this.auto_close_brackets || false }-*/;
+  private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+
+  protected EditPreferences() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
new file mode 100644
index 0000000..73d4ca0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.diff.PreferencesBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class MyDiffPreferencesScreen extends SettingsScreen {
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    PreferencesBox pb = new PreferencesBox(null);
+    pb.set(DiffPreferences.create(Gerrit.getAccountDiffPreference()));
+    FlowPanel p = new FlowPanel();
+    p.setStyleName(pb.getStyle().dialog());
+    p.add(pb);
+    add(p);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    display();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java
new file mode 100644
index 0000000..424b5d5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.editor.EditPreferencesBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class MyEditPreferencesScreen extends SettingsScreen {
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    EditPreferencesBox pb = new EditPreferencesBox(null);
+    pb.set(EditPreferences.create(Gerrit.getEditPreferences()));
+    FlowPanel p = new FlowPanel();
+    p.setStyleName(pb.getStyle().dialog());
+    p.add(pb);
+    add(p);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    display();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
index dc35b1e..99d791b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
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 2f3a819..e8c58ef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -37,6 +37,8 @@
 
     linkByGerrit(Util.C.tabAccountSummary(), PageLinks.SETTINGS);
     linkByGerrit(Util.C.tabPreferences(), PageLinks.SETTINGS_PREFERENCES);
+    linkByGerrit(Util.C.tabDiffPreferences(), PageLinks.SETTINGS_DIFF_PREFERENCES);
+    linkByGerrit(Util.C.tabEditPreferences(), PageLinks.SETTINGS_EDIT_PREFERENCES);
     linkByGerrit(Util.C.tabWatchedProjects(), PageLinks.SETTINGS_PROJECTS);
     linkByGerrit(Util.C.tabContactInformation(), PageLinks.SETTINGS_CONTACT);
     if (Gerrit.info().hasSshd()) {
@@ -45,7 +47,7 @@
     if (Gerrit.info().auth().isHttpPasswordSettingsEnabled()) {
       linkByGerrit(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
     }
-    if (Gerrit.info().receive().enableSignedPush()) {
+    if (Gerrit.info().gerrit().editGpgKeys()) {
       linkByGerrit(Util.C.tabGpgKeys(), PageLinks.SETTINGS_GPGKEYS);
     }
     linkByGerrit(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 66a64b4..511be5f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -43,6 +43,7 @@
   String useSignedOffBy();
   String createNewChangeForAllNotInTarget();
   String enableSignedPush();
+  String requireSignedPush();
   String requireChangeID();
   String headingMaxObjectSizeLimit();
   String headingGroupOptions();
@@ -104,6 +105,7 @@
   String buttonDeleteBranch();
   String saveHeadButton();
   String cancelHeadButton();
+  String columnTagName();
 
   String groupItemHelp();
 
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 952ea5f..7a8888c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -25,6 +25,7 @@
 useSignedOffBy = Require <code>Signed-off-by</code> in commit message
 createNewChangeForAllNotInTarget = Create a new change for every commit not in the target branch
 enableSignedPush = Enable signed push
+requireSignedPush = Require signed push
 requireChangeID = Require <code>Change-Id</code> in commit message
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
@@ -83,6 +84,7 @@
 buttonDeleteBranch = Delete
 saveHeadButton = Save
 cancelHeadButton = Cancel
+columnTagName = Tag Name
 
 groupItemHelp = group
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 134f869..bfe7787 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -97,10 +98,10 @@
     setPageTitle(Util.C.groupListTitle());
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev = PagingHyperlink.createPrev();
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next = PagingHyperlink.createNext();
     next.setVisible(false);
 
     groups = new GroupTable(PageLinks.ADMIN_GROUPS);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
new file mode 100644
index 0000000..6349803
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.http.client.URL;
+
+abstract class PaginatedProjectScreen extends ProjectScreen {
+  protected int pageSize;
+  protected String match;
+  protected int start;
+
+  PaginatedProjectScreen(Project.NameKey toShow) {
+    super(toShow);
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
+  }
+
+  protected void parseToken(String token) {
+    for (String kvPair : token.split("[,;&/?]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("filter".equals(kv[0])) {
+        match = URL.decodeQueryString(kv[1]);
+      }
+
+      if ("skip".equals(kv[0])
+          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
+        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
+      }
+    }
+  }
+
+  protected void parseToken() {
+    parseToken(getToken());
+  }
+
+  protected String getTokenForScreen(String filter, int skip) {
+    String token = getScreenToken();
+    if (filter != null && !filter.isEmpty()) {
+      token += "?filter=" + URL.encodeQueryString(filter);
+    }
+    if (skip > 0) {
+      if (token.contains("?filter=")) {
+        token += ",";
+      } else {
+        token += "?";
+      }
+      token += "skip=" + skip;
+    }
+    return token;
+  }
+
+  protected abstract String getScreenToken();
+
+  protected void setupNavigationLink(Hyperlink link, String filter, int skip) {
+    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
+    link.setVisible(true);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 9a13158..1a28685 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
@@ -37,6 +37,7 @@
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
@@ -53,7 +54,6 @@
 import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
@@ -74,7 +74,7 @@
 import java.util.List;
 import java.util.Set;
 
-public class ProjectBranchesScreen extends ProjectScreen {
+public class ProjectBranchesScreen extends PaginatedProjectScreen {
   private Hyperlink prev;
   private Hyperlink next;
   private BranchesTable branchTable;
@@ -83,56 +83,16 @@
   private HintTextBox nameTxtBox;
   private HintTextBox irevTxtBox;
   private FlowPanel addPanel;
-  private int pageSize;
-  private int start;
   private NpTextBox filterTxt;
-  private String match;
   private Query query;
 
   public ProjectBranchesScreen(final Project.NameKey toShow) {
     super(toShow);
-    pageSize = Gerrit.getUserPreferences().changesPerPage();
   }
 
-  private void parseToken() {
-    String token = getToken();
-
-    for (String kvPair : token.split("[,;&/?]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0])
-          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
-  }
-
-  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-
-  private String getTokenForScreen(String filter, int skip) {
-    String token = PageLinks.toProjectBranches(getProjectKey());
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
+  @Override
+  public String getScreenToken() {
+    return PageLinks.toProjectBranches(getProjectKey());
   }
 
   @Override
@@ -147,7 +107,7 @@
           }
         });
     query = new Query(match).start(start).run();
-    savedPanel = BRANCH;
+    savedPanel = BRANCHES;
   }
 
   private void updateForm() {
@@ -162,10 +122,10 @@
     super.onInitUI();
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev = PagingHyperlink.createPrev();
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next = PagingHyperlink.createNext();
     next.setVisible(false);
 
     addPanel = new FlowPanel();
@@ -215,6 +175,7 @@
     branchTable = new BranchesTable();
 
     delBranch = new Button(Util.C.buttonDeleteBranch());
+    delBranch.setStyleName(Gerrit.RESOURCES.css().branchTableDeleteButton());
     delBranch.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
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 eefd199..c6bd1d1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -84,6 +84,7 @@
   private ListBox contentMerge;
   private ListBox newChangeForAllNotInTarget;
   private ListBox enableSignedPush;
+  private ListBox requireSignedPush;
   private NpTextBox maxObjectSizeLimit;
   private Label effectiveMaxObjectSizeLimit;
   private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
@@ -247,6 +248,9 @@
       enableSignedPush = newInheritedBooleanBox();
       saveEnabler.listenTo(enableSignedPush);
       grid.add(Util.C.enableSignedPush(), enableSignedPush);
+      requireSignedPush = newInheritedBooleanBox();
+      saveEnabler.listenTo(requireSignedPush);
+      grid.add(Util.C.requireSignedPush(), requireSignedPush);
     }
 
     maxObjectSizeLimit = new NpTextBox();
@@ -326,6 +330,9 @@
   }
 
   private void setBool(ListBox box, InheritedBooleanInfo inheritedBoolean) {
+    if (box == null) {
+      return;
+    }
     int inheritedIndex = -1;
     for (int i = 0; i < box.getItemCount(); i++) {
       if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
@@ -372,8 +379,9 @@
     setBool(contentMerge, result.useContentMerge());
     setBool(newChangeForAllNotInTarget, result.createNewChangeForAllNotInTarget());
     setBool(requireChangeID, result.requireChangeId());
-    if (enableSignedPush != null) {
+    if (Gerrit.info().receive().enableSignedPush()) {
       setBool(enableSignedPush, result.enableSignedPush());
+      setBool(requireSignedPush, result.requireSignedPush());
     }
     setSubmitType(result.submitType());
     setState(result.state());
@@ -644,12 +652,14 @@
   private void doSave() {
     enableForm(false);
     saveProject.setEnabled(false);
-    InheritableBoolean sp = enableSignedPush != null
+    InheritableBoolean esp = enableSignedPush != null
         ? getBool(enableSignedPush) : null;
+    InheritableBoolean rsp = requireSignedPush != null
+        ? getBool(requireSignedPush) : null;
     ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
         getBool(contributorAgreements), getBool(contentMerge),
         getBool(signedOffBy), getBool(newChangeForAllNotInTarget), getBool(requireChangeID),
-        sp,
+        esp, rsp,
         maxObjectSizeLimit.getText().trim(),
         SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
         ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
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 f9904cd..4503265 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -26,14 +26,13 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.client.ui.ProjectSearchLink;
 import com.google.gerrit.client.ui.ProjectsTable;
-import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyUpEvent;
 import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -44,37 +43,21 @@
 
 import java.util.List;
 
-public class ProjectListScreen extends Screen {
+public class ProjectListScreen extends PaginatedProjectScreen {
   private Hyperlink prev;
   private Hyperlink next;
   private ProjectsTable projects;
   private NpTextBox filterTxt;
-  private int pageSize;
 
-  private String match = "";
-  private int start;
   private Query query;
 
   public ProjectListScreen() {
-    pageSize = Gerrit.getUserPreferences().changesPerPage();
+    super(null);
   }
 
   public ProjectListScreen(String params) {
     this();
-    for (String kvPair : params.split("[,;&]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0]) && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
+    parseToken(params);
   }
 
   @Override
@@ -83,25 +66,9 @@
     query = new Query(match).start(start).run();
   }
 
-  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-
-  private String getTokenForScreen(String filter, int skip) {
-    String token = ADMIN_PROJECTS;
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
+  @Override
+  public String getScreenToken() {
+    return ADMIN_PROJECTS;
   }
 
   @Override
@@ -110,10 +77,10 @@
     setPageTitle(Util.C.projectListTitle());
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev = PagingHyperlink.createPrev();
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next = PagingHyperlink.createNext();
     next.setVisible(false);
 
     projects = new ProjectsTable() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
index fdf3ab8..a63dae4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -19,9 +19,10 @@
 
 public abstract class ProjectScreen extends Screen {
   public static final String INFO = "info";
-  public static final String BRANCH = "branches";
+  public static final String BRANCHES = "branches";
   public static final String ACCESS = "access";
   public static final String DASHBOARDS = "dashboards";
+  public static final String TAGS = "tags";
 
   protected static String savedPanel;
   protected static Project.NameKey savedKey;
@@ -47,7 +48,9 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    setPageTitle(Util.M.project(name.get()));
+    if (name != null) {
+      setPageTitle(Util.M.project(name.get()));
+    }
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
new file mode 100644
index 0000000..1b5d2e8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import static com.google.gerrit.client.ui.Util.highlight;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.projects.TagInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.client.ui.PagingHyperlink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+import java.util.List;
+
+public class ProjectTagsScreen extends PaginatedProjectScreen {
+  private NpTextBox filterTxt;
+  private Query query;
+  private Hyperlink prev;
+  private Hyperlink next;
+  private TagsTable tagsTable;
+
+  public ProjectTagsScreen(Project.NameKey toShow) {
+    super(toShow);
+  }
+
+  @Override
+  public String getScreenToken() {
+    return PageLinks.toProjectTags(getProjectKey());
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    initPageHeader();
+    prev = PagingHyperlink.createPrev();
+    prev.setVisible(false);
+
+    next = PagingHyperlink.createNext();
+    next.setVisible(false);
+
+    tagsTable = new TagsTable();
+
+    HorizontalPanel buttons = new HorizontalPanel();
+    buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
+    buttons.add(prev);
+    buttons.add(next);
+    add(tagsTable);
+    add(buttons);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    query = new Query(match).start(start).run();
+    savedPanel = TAGS;
+  }
+
+  private void initPageHeader() {
+    parseToken();
+    HorizontalPanel hp = new HorizontalPanel();
+    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    Label filterLabel = new Label(Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    hp.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(match);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        Query q = new Query(filterTxt.getValue());
+        if (match.equals(q.qMatch)) {
+          q.start(start);
+        } else if (query == null) {
+          q.run();
+          query = q;
+        }
+      }
+    });
+    hp.add(filterTxt);
+    add(hp);
+  }
+
+  private class TagsTable extends NavigationTable<TagInfo> {
+
+    TagsTable() {
+      table.setWidth("");
+      table.setText(0, 0, Util.C.columnTagName());
+      table.setText(0, 1, Util.C.columnBranchRevision());
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    void display(List<TagInfo> tags) {
+      displaySubset(tags, 0, tags.size());
+    }
+
+    void displaySubset(List<TagInfo> tags, int fromIndex, int toIndex) {
+      while (1 < table.getRowCount()) {
+        table.removeRow(table.getRowCount() - 1);
+      }
+
+      for (TagInfo k : tags.subList(fromIndex, toIndex)) {
+        int row = table.getRowCount();
+        table.insertRow(row);
+        applyDataRowStyle(row);
+        populate(row, k);
+      }
+    }
+
+    void populate(int row, TagInfo k) {
+      table.setWidget(row, 0, new InlineHTML(highlight(k.getShortName(), match)));
+
+      if (k.revision() != null) {
+        table.setText(row, 1, k.revision());
+      } else {
+        table.setText(row, 1, "");
+      }
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
+      fmt.addStyleName(row, 0, dataCellStyle);
+      fmt.addStyleName(row, 1, dataCellStyle);
+
+      setRowItem(row, k);
+    }
+
+    @Override
+    protected void onOpenRow(int row) {
+      if (row > 0) {
+        movePointerTo(row);
+      }
+    }
+
+    @Override
+    protected Object getRowItemKey(TagInfo item) {
+      return item.ref();
+    }
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (match != null) {
+      filterTxt.setCursorPos(match.length());
+    }
+    filterTxt.setFocus(true);
+  }
+
+  private class Query {
+    private String qMatch;
+    private int qStart;
+
+    Query(String match) {
+      this.qMatch = match;
+    }
+
+    Query start(int start) {
+      this.qStart = start;
+      return this;
+    }
+
+    Query run() {
+      // Retrieve one more tag than page size to determine if there are more
+      // tags to display
+      ProjectApi.getTags(getProjectKey(), pageSize + 1, qStart, qMatch,
+          new ScreenLoadCallback<JsArray<TagInfo>>(ProjectTagsScreen.this) {
+            @Override
+            public void preDisplay(JsArray<TagInfo> result) {
+              if (!isAttached()) {
+                // View has been disposed.
+              } else if (query == Query.this) {
+                query = null;
+                showList(result);
+              } else {
+                query.run();
+              }
+            }
+          });
+      return this;
+    }
+
+    void showList(JsArray<TagInfo> result) {
+      setToken(getTokenForScreen(qMatch, qStart));
+      ProjectTagsScreen.this.match = qMatch;
+      ProjectTagsScreen.this.start = qStart;
+
+      if (result.length() <= pageSize) {
+        tagsTable.display(Natives.asList(result));
+        next.setVisible(false);
+      } else {
+        tagsTable.displaySubset(Natives.asList(result), 0,
+            result.length() - 1);
+        setupNavigationLink(next, qMatch, qStart + pageSize);
+      }
+      if (qStart > 0) {
+        setupNavigationLink(prev, qMatch, qStart - pageSize);
+      } else {
+        prev.setVisible(false);
+      }
+
+      if (!isCurrentView()) {
+        display();
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index b9ea8e0..eabf0e5 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
@@ -28,6 +28,7 @@
 import com.google.gerrit.client.changes.StarredChanges;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.diff.DiffApi;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
 import com.google.gerrit.client.info.ActionInfo;
 import com.google.gerrit.client.info.ChangeInfo;
@@ -37,6 +38,8 @@
 import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.info.FileInfo;
+import com.google.gerrit.client.info.GpgKeyInfo;
+import com.google.gerrit.client.info.PushCertificateInfo;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -66,6 +69,7 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.SelectElement;
+import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -86,6 +90,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.SimplePanel;
@@ -109,17 +114,18 @@
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   interface Style extends CssResource {
-    String labelName();
     String avatar();
-    String label_user();
-    String label_ok();
-    String label_reject();
+    String hashtagName();
+    String highlight();
+    String labelName();
     String label_may();
     String label_need();
+    String label_ok();
+    String label_reject();
+    String label_user();
+    String pushCertStatus();
     String replyBox();
     String selected();
-    String highlight();
-    String hashtagName();
   }
 
   static ChangeScreen get(NativeEvent in) {
@@ -161,8 +167,14 @@
   @UiField Reviewers reviewers;
   @UiField Hashtags hashtags;
   @UiField Element hashtagTableRow;
+
   @UiField FlowPanel ownerPanel;
   @UiField InlineHyperlink ownerLink;
+
+  @UiField Element uploaderRow;
+  @UiField FlowPanel uploaderPanel;
+  @UiField InlineLabel uploaderName;
+
   @UiField Element statusText;
   @UiField Image projectSettings;
   @UiField AnchorElement projectSettingsLink;
@@ -286,11 +298,19 @@
     p.add(extensionPanel);
   }
 
+  private boolean enableSignedPush() {
+    return Gerrit.info().receive().enableSignedPush();
+  }
+
   void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
     RestApi call = ChangeApi.detail(changeId.get());
-    ChangeList.addOptions(call, EnumSet.of(
-      ListChangesOption.CHANGE_ACTIONS,
-      ListChangesOption.ALL_REVISIONS));
+    EnumSet<ListChangesOption> opts = EnumSet.of(
+      ListChangesOption.ALL_REVISIONS,
+      ListChangesOption.CHANGE_ACTIONS);
+    if (enableSignedPush()) {
+      opts.add(ListChangesOption.PUSH_CERTIFICATES);
+    }
+    ChangeList.addOptions(call, opts);
     if (!fg) {
       call.background();
     }
@@ -1140,12 +1160,14 @@
   }
 
   private void renderChangeInfo(ChangeInfo info) {
+    RevisionInfo revisionInfo = info.revision(revision);
     changeInfo = info;
     lastDisplayedUpdate = info.updated();
 
     labels.set(info);
 
     renderOwner(info);
+    renderUploader(info, revisionInfo);
     renderActionTextDate(info);
     renderDiffBaseListBox(info);
     initReplyButton(info, revision);
@@ -1182,7 +1204,7 @@
     // render it faster.
     if (!info.status().isOpen()
         || !revision.equals(info.currentRevision())
-        || info.revision(revision).isEdit()) {
+        || revisionInfo.isEdit()) {
       setVisible(strategy, false);
     }
 
@@ -1193,7 +1215,6 @@
     quickApprove.setVisible(false);
     actions.reloadRevisionActions(emptyMap);
 
-    RevisionInfo revisionInfo = info.revision(revision);
     boolean current = revision.equals(info.currentRevision())
         && !revisionInfo.isEdit();
 
@@ -1231,17 +1252,12 @@
 
   private void renderOwner(ChangeInfo info) {
     // TODO info card hover
-    String name = info.owner().name() != null
-        ? info.owner().name()
-        : Gerrit.info().user().anonymousCowardName();
-
+    String name = name(info.owner());
     if (info.owner().avatar(AvatarInfo.DEFAULT_SIZE) != null) {
       ownerPanel.insert(new AvatarImage(info.owner()), 0);
     }
     ownerLink.setText(name);
-    ownerLink.setTitle(info.owner().email() != null
-        ? info.owner().email()
-        : name);
+    ownerLink.setTitle(email(info.owner(), name));
     ownerLink.setTargetHistoryToken(PageLinks.toAccountQuery(
         info.owner().name() != null
         ? info.owner().name()
@@ -1250,6 +1266,81 @@
         : String.valueOf(info.owner()._accountId()), Change.Status.NEW));
   }
 
+  private void renderUploader(ChangeInfo changeInfo, RevisionInfo revInfo) {
+    AccountInfo uploader = revInfo.uploader();
+    boolean isOwner = uploader == null
+        || uploader._accountId() == changeInfo.owner()._accountId();
+    renderPushCertificate(revInfo, isOwner ? ownerPanel : uploaderPanel);
+    if (isOwner) {
+      uploaderRow.getStyle().setDisplay(Display.NONE);
+      return;
+    }
+    uploaderRow.getStyle().setDisplay(Display.TABLE_ROW);
+
+    if (uploader.avatar(AvatarInfo.DEFAULT_SIZE) != null) {
+      uploaderPanel.insert(new AvatarImage(uploader), 0);
+    }
+    String name = name(uploader);
+    uploaderName.setText(name);
+    uploaderName.setTitle(email(uploader, name));
+  }
+
+  private void renderPushCertificate(RevisionInfo revInfo, FlowPanel panel) {
+    if (!enableSignedPush()) {
+      return;
+    }
+    Image status = new Image();
+    panel.add(status);
+    status.setStyleName(style.pushCertStatus());
+    if (!revInfo.hasPushCertificate()
+        || revInfo.pushCertificate().key() == null) {
+      status.setResource(Gerrit.RESOURCES.question());
+      status.setTitle(Util.C.pushCertMissing());
+      return;
+    }
+    PushCertificateInfo certInfo = revInfo.pushCertificate();
+    GpgKeyInfo.Status s = certInfo.key().status();
+    switch (s) {
+      case BAD:
+        status.setResource(Gerrit.RESOURCES.redNot());
+        status.setTitle(problems(Util.C.pushCertBad(), certInfo));
+        break;
+      case OK:
+        status.setResource(Gerrit.RESOURCES.warning());
+        status.setTitle(problems(Util.C.pushCertOk(), certInfo));
+        break;
+      case TRUSTED:
+        status.setResource(Gerrit.RESOURCES.greenCheck());
+        status.setTitle(Util.C.pushCertTrusted());
+        break;
+    }
+  }
+
+  private static String name(AccountInfo info) {
+    return info.name() != null
+        ? info.name()
+        : Gerrit.info().user().anonymousCowardName();
+  }
+
+  private static String email(AccountInfo info, String name) {
+    return info.email() != null ? info.email() : name;
+  }
+
+  private static String problems(String msg, PushCertificateInfo info) {
+    if (info.key() == null
+        || !info.key().hasProblems()
+        || info.key().problems().length() == 0) {
+      return msg;
+    }
+
+    StringBuilder sb = new StringBuilder();
+    sb.append(msg).append(':');
+    for (String problem : Natives.asList(info.key().problems())) {
+      sb.append('\n').append(problem);
+    }
+    return sb.toString();
+  }
+
   private void renderSubmitType(String action) {
     try {
       SubmitType type = SubmitType.valueOf(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 06ae4ca..c643072 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -315,7 +315,7 @@
       background-color: trimColor;
     }
 
-    .ownerPanel img {
+    .ownerPanel img, .uploaderPanel img {
       margin: 0 2px 0 0;
       width: 16px;
       height: 16px !important;
@@ -334,6 +334,10 @@
     .changeExtension {
       padding-top: 5px;
     }
+
+    .pushCertStatus {
+      padding-left: 5px;
+    }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.cs2}'>
@@ -421,6 +425,14 @@
                 </g:FlowPanel>
               </td>
             </tr>
+            <tr ui:field='uploaderRow'>
+              <th><ui:msg>Uploader</ui:msg></th>
+              <td>
+                <g:FlowPanel ui:field='uploaderPanel' styleName='{style.uploaderPanel}'>
+                  <g:InlineLabel ui:field='uploaderName'/>
+                </g:FlowPanel>
+              </td>
+            </tr>
             <tr>
               <th><ui:msg>Reviewers</ui:msg></th>
               <td>
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 ad61446..d1ca517 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.client.change;
 
+import static com.google.gerrit.client.FormatUtil.formatAbsBytes;
+import static com.google.gerrit.client.FormatUtil.formatBytes;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
@@ -457,8 +460,12 @@
     private ProgressBar meter;
     private String lastPath = "";
 
+    private boolean hasBinaryFile;
+    private boolean hasNonBinaryFile;
     private int inserted;
     private int deleted;
+    private long bytesInserted;
+    private long bytesDeleted;
 
     private DisplayCommand(NativeMap<FileInfo> map,
         JsArray<FileInfo> list,
@@ -513,11 +520,23 @@
     private void computeInsertedDeleted() {
       inserted = 0;
       deleted = 0;
+      bytesInserted = 0;
+      bytesDeleted = 0;
       for (int i = 0; i < list.length(); i++) {
         FileInfo info = list.get(i);
-        if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
-          inserted += info.linesInserted();
-          deleted += info.linesDeleted();
+        if (!Patch.COMMIT_MSG.equals(info.path())) {
+          if (!info.binary()) {
+            hasNonBinaryFile = true;
+            inserted += info.linesInserted();
+            deleted += info.linesDeleted();
+          } else {
+            hasBinaryFile = true;
+            if (info.sizeDelta() >= 0) {
+              bytesInserted += info.sizeDelta();
+            } else {
+              bytesDeleted += info.sizeDelta();
+            }
+          }
         }
       }
     }
@@ -750,6 +769,8 @@
               .append(info.linesDeleted());
           }
         }
+      } else if (info.binary()) {
+        sb.append(formatBytes(info.sizeDelta()));
       }
       sb.closeTd();
     }
@@ -798,9 +819,18 @@
       sb.openTd().setAttribute("colspan", 3).closeTd(); // comments
 
       // delta1
-      sb.openTh().setStyleName(R.css().deltaColumn1())
-        .append(Util.M.patchTableSize_Modify(inserted, deleted))
-        .closeTh();
+      sb.openTh().setStyleName(R.css().deltaColumn1());
+      if (hasNonBinaryFile) {
+        sb.append(Util.M.patchTableSize_Modify(inserted, deleted));
+      }
+      if (hasBinaryFile) {
+        if (hasNonBinaryFile) {
+          sb.br();
+        }
+        sb.append(Util.M.patchTableSize_ModifyBinaryFiles(
+            formatAbsBytes(bytesInserted), formatAbsBytes(bytesDeleted)));
+      }
+      sb.closeTh();
 
       // delta2
       sb.openTh().setStyleName(R.css().deltaColumn2());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index 9bb3a76..8b559e3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -138,15 +138,9 @@
   }
 
   private void setName(boolean open) {
-    name.setInnerText(open ? authorName(info) : elide(authorName(info), 20));
-  }
-
-  private static String elide(final String s, final int len) {
-    if (s == null || s.length() <= len || len <= 10) {
-      return s;
-    }
-    int i = (len - 3) / 2;
-    return s.substring(0, i) + "..." + s.substring(s.length() - i);
+    name.setInnerText(open
+        ? authorName(info)
+        : com.google.gerrit.common.FormatUtil.elide(authorName(info), 20));
   }
 
   void autoOpen() {
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 ae1a608..9612f71 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
@@ -228,11 +228,9 @@
           new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision));
     } else {
       // TODO(sbeller): show only on latest revision
-      if (info.status().isOpen()) {
-        ChangeApi.change(info.legacyId().get()).view("submitted_together")
-            .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
-                info.project(), revision));
-      }
+      ChangeApi.change(info.legacyId().get()).view("submitted_together")
+          .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
+              info.project(), revision));
     }
   }
 
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 f0101cb..bde9755 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -75,7 +75,7 @@
 
 .deltaColumn1 {
   white-space: nowrap;
-  text-align: right;
+  text-align: right !important;
 }
 
 .deltaColumn2 {
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 ca4c633..1fb997f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -202,4 +202,9 @@
   String diffAllUnified();
 
   String votable();
+
+  String pushCertMissing();
+  String pushCertBad();
+  String pushCertOk();
+  String pushCertTrusted();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index e348161..a5fa7b4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -184,3 +184,8 @@
 diffAllUnified = All Unified
 
 votable = Votable:
+
+pushCertMissing = This patch set was created without a push certificate
+pushCertBad = Push certificate is invalid
+pushCertOk = Push certificate is valid, but key is not trusted
+pushCertTrusted = Push certificate is valid and key is trusted
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 2d3644e..ef74a65 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -34,6 +34,8 @@
   String patchTableComments(@PluralCount int count);
   String patchTableDrafts(@PluralCount int count);
   String patchTableSize_Modify(int insertions, int deletions);
+  String patchTableSize_ModifyBinaryFiles(String bytesInserted,
+      String bytesDeleted);
   String patchTableSize_LongModify(int insertions, int deletions);
   String patchTableSize_Lines(@PluralCount int insertions);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index c109794..67ef2c3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -17,6 +17,7 @@
 patchTableComments = {0} comments
 patchTableDrafts = {0} drafts
 patchTableSize_Modify = +{0}, -{1}
+patchTableSize_ModifyBinaryFiles = +{0}, -{1}
 patchTableSize_LongModify = {0} inserted, {1} deleted
 patchTableSize_Lines = {0} lines
 
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 6e65ccd..28812ac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -52,7 +52,7 @@
   }
 
   private static class EmailConfirmationInput extends JavaScriptObject {
-    final native void setToken(String t) /*-{ this.t = t; }-*/;
+    final native void setToken(String token) /*-{ this.token = token; }-*/;
 
     static EmailConfirmationInput create() {
       return createObject().cast();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index 4265203..3364490 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -35,6 +35,8 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Visibility;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyDownEvent;
@@ -54,6 +56,7 @@
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.ToggleButton;
+import com.google.gwt.user.client.ui.UIObject;
 
 import net.codemirror.mode.ModeInfo;
 import net.codemirror.mode.ModeInjector;
@@ -62,11 +65,11 @@
 import java.util.Objects;
 
 /** Displays current diff preferences. */
-class PreferencesBox extends Composite {
+public class PreferencesBox extends Composite {
   interface Binder extends UiBinder<HTMLPanel, PreferencesBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  interface Style extends CssResource {
+  public interface Style extends CssResource {
     String dialog();
   }
 
@@ -76,6 +79,7 @@
   private Timer updateContextTimer;
 
   @UiField Style style;
+  @UiField Element header;
   @UiField Anchor close;
   @UiField ListBox ignoreWhitespace;
   @UiField NpIntTextBox tabWidth;
@@ -87,6 +91,7 @@
   @UiField ToggleButton whitespaceErrors;
   @UiField ToggleButton showTabs;
   @UiField ToggleButton lineNumbers;
+  @UiField Element leftSideLabel;
   @UiField ToggleButton leftSide;
   @UiField ToggleButton emptyPane;
   @UiField ToggleButton topMenu;
@@ -95,17 +100,24 @@
   @UiField ToggleButton expandAllComments;
   @UiField ToggleButton renderEntireFile;
   @UiField ListBox theme;
+  @UiField Element modeLabel;
   @UiField ListBox mode;
   @UiField Button apply;
   @UiField Button save;
 
-  PreferencesBox(SideBySide view) {
+  public PreferencesBox(SideBySide view) {
     this.view = view;
 
     initWidget(uiBinder.createAndBindUi(this));
     initIgnoreWhitespace();
     initTheme();
-    initMode();
+
+    if (view != null) {
+      initMode();
+    } else {
+      UIObject.setVisible(header, false);
+      apply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
+    }
   }
 
   @Override
@@ -113,40 +125,47 @@
     super.onLoad();
 
     save.setVisible(Gerrit.isSignedIn());
-    addDomHandler(new KeyDownHandler() {
-      @Override
-      public void onKeyDown(KeyDownEvent event) {
-        if (event.getNativeKeyCode() == KEY_ESCAPE
-            || event.getNativeKeyCode() == ',') {
-          close();
-        }
-      }
-    }, KeyDownEvent.getType());
 
-    updateContextTimer = new Timer() {
-      @Override
-      public void run() {
-        if (prefs.context() == WHOLE_FILE_CONTEXT) {
-          contextEntireFile.setValue(true);
+    if (view != null) {
+      addDomHandler(new KeyDownHandler() {
+        @Override
+        public void onKeyDown(KeyDownEvent event) {
+          if (event.getNativeKeyCode() == KEY_ESCAPE
+              || event.getNativeKeyCode() == ',') {
+            close();
+          }
         }
-        if (view.canRenderEntireFile(prefs)) {
-          renderEntireFile.setEnabled(true);
-          renderEntireFile.setValue(prefs.renderEntireFile());
-        } else {
-          renderEntireFile.setValue(false);
-          renderEntireFile.setEnabled(false);
+      }, KeyDownEvent.getType());
+
+      updateContextTimer = new Timer() {
+        @Override
+        public void run() {
+          if (prefs.context() == WHOLE_FILE_CONTEXT) {
+            contextEntireFile.setValue(true);
+          }
+          if (view.canRenderEntireFile(prefs)) {
+            renderEntireFile.setEnabled(true);
+            renderEntireFile.setValue(prefs.renderEntireFile());
+          } else {
+            renderEntireFile.setValue(false);
+            renderEntireFile.setEnabled(false);
+          }
+          view.setContext(prefs.context());
         }
-        view.setContext(prefs.context());
-      }
-    };
+      };
+    }
   }
 
-  void set(DiffPreferences prefs) {
+  public Style getStyle() {
+    return style;
+  }
+
+  public void set(DiffPreferences prefs) {
     this.prefs = prefs;
 
     setIgnoreWhitespace(prefs.ignoreWhitespace());
     tabWidth.setIntValue(prefs.tabSize());
-    if (Patch.COMMIT_MSG.equals(view.getPath())) {
+    if (view != null && Patch.COMMIT_MSG.equals(view.getPath())) {
       lineLength.setEnabled(false);
       lineLength.setIntValue(72);
     } else {
@@ -157,17 +176,22 @@
     whitespaceErrors.setValue(prefs.showWhitespaceErrors());
     showTabs.setValue(prefs.showTabs());
     lineNumbers.setValue(prefs.showLineNumbers());
-    leftSide.setValue(view.diffTable.isVisibleA());
     emptyPane.setValue(!prefs.hideEmptyPane());
-    leftSide.setEnabled(!(prefs.hideEmptyPane()
-        && view.diffTable.getChangeType() == ChangeType.ADDED));
+    if (view != null) {
+      leftSide.setValue(view.diffTable.isVisibleA());
+      leftSide.setEnabled(!(prefs.hideEmptyPane()
+          && view.diffTable.getChangeType() == ChangeType.ADDED));
+    } else {
+      UIObject.setVisible(leftSideLabel, false);
+      leftSide.setVisible(false);
+    }
     topMenu.setValue(!prefs.hideTopMenu());
     autoHideDiffTableHeader.setValue(!prefs.autoHideDiffTableHeader());
     manualReview.setValue(prefs.manualReview());
     expandAllComments.setValue(prefs.expandAllComments());
     setTheme(prefs.theme());
 
-    if (view.canRenderEntireFile(prefs)) {
+    if (view == null || view.canRenderEntireFile(prefs)) {
       renderEntireFile.setValue(prefs.renderEntireFile());
       renderEntireFile.setEnabled(true);
     } else {
@@ -175,22 +199,31 @@
       renderEntireFile.setEnabled(false);
     }
 
-    mode.setEnabled(prefs.syntaxHighlighting());
-    if (prefs.syntaxHighlighting()) {
-      setMode(view.getCmFromSide(DisplaySide.B).getStringOption("mode"));
+    if (view != null) {
+      mode.setEnabled(prefs.syntaxHighlighting());
+      if (prefs.syntaxHighlighting()) {
+        setMode(view.getCmFromSide(DisplaySide.B).getStringOption("mode"));
+      }
+    } else {
+      UIObject.setVisible(modeLabel, false);
+      mode.setVisible(false);
     }
 
-    switch (view.getIntraLineStatus()) {
-      case OFF:
-      case OK:
-        intralineDifference.setValue(prefs.intralineDifference());
-        break;
+    if (view != null) {
+      switch (view.getIntraLineStatus()) {
+        case OFF:
+        case OK:
+          intralineDifference.setValue(prefs.intralineDifference());
+          break;
 
-      case TIMEOUT:
-      case FAILURE:
-        intralineDifference.setValue(false);
-        intralineDifference.setEnabled(false);
-        break;
+        case TIMEOUT:
+        case FAILURE:
+          intralineDifference.setValue(false);
+          intralineDifference.setEnabled(false);
+          break;
+      }
+    } else {
+      intralineDifference.setValue(prefs.intralineDifference());
     }
 
     if (prefs.context() == WHOLE_FILE_CONTEXT) {
@@ -207,13 +240,17 @@
   void onIgnoreWhitespace(@SuppressWarnings("unused") ChangeEvent e) {
     prefs.ignoreWhitespace(Whitespace.valueOf(
         ignoreWhitespace.getValue(ignoreWhitespace.getSelectedIndex())));
-    view.reloadDiffInfo();
+    if (view != null) {
+      view.reloadDiffInfo();
+    }
   }
 
   @UiHandler("intralineDifference")
   void onIntralineDifference(ValueChangeEvent<Boolean> e) {
     prefs.intralineDifference(e.getValue());
-    view.setShowIntraline(prefs.intralineDifference());
+    if (view != null) {
+      view.setShowIntraline(prefs.intralineDifference());
+    }
   }
 
   @UiHandler("context")
@@ -239,7 +276,9 @@
       return;
     }
     prefs.context(c);
-    updateContextTimer.schedule(200);
+    if (view != null) {
+      updateContextTimer.schedule(200);
+    }
   }
 
   @UiHandler("contextEntireFile")
@@ -257,7 +296,9 @@
       context.setFocus(true);
       context.setSelectionRange(0, context.getText().length());
     }
-    updateContextTimer.schedule(200);
+    if (view != null) {
+      updateContextTimer.schedule(200);
+    }
   }
 
   @UiHandler("tabWidth")
@@ -265,14 +306,16 @@
     String v = e.getValue();
     if (v != null && v.length() > 0) {
       prefs.tabSize(Math.max(1, Integer.parseInt(v)));
-      view.operation(new Runnable() {
-        @Override
-        public void run() {
-          int v = prefs.tabSize();
-          view.getCmFromSide(DisplaySide.A).setOption("tabSize", v);
-          view.getCmFromSide(DisplaySide.B).setOption("tabSize", v);
-        }
-      });
+      if (view != null) {
+        view.operation(new Runnable() {
+          @Override
+          public void run() {
+            int v = prefs.tabSize();
+            view.getCmFromSide(DisplaySide.A).setOption("tabSize", v);
+            view.getCmFromSide(DisplaySide.B).setOption("tabSize", v);
+          }
+        });
+      }
     }
   }
 
@@ -281,30 +324,38 @@
     String v = e.getValue();
     if (v != null && v.length() > 0) {
       prefs.lineLength(Math.max(1, Integer.parseInt(v)));
-      view.operation(new Runnable() {
-        @Override
-        public void run() {
-          view.setLineLength(prefs.lineLength());
-        }
-      });
+      if (view != null) {
+        view.operation(new Runnable() {
+          @Override
+          public void run() {
+            view.setLineLength(prefs.lineLength());
+          }
+        });
+      }
     }
   }
   @UiHandler("expandAllComments")
   void onExpandAllComments(ValueChangeEvent<Boolean> e) {
     prefs.expandAllComments(e.getValue());
-    view.getCommentManager().setExpandAllComments(prefs.expandAllComments());
+    if (view != null) {
+      view.getCommentManager().setExpandAllComments(prefs.expandAllComments());
+    }
   }
 
   @UiHandler("showTabs")
   void onShowTabs(ValueChangeEvent<Boolean> e) {
     prefs.showTabs(e.getValue());
-    view.setShowTabs(prefs.showTabs());
+    if (view != null) {
+      view.setShowTabs(prefs.showTabs());
+    }
   }
 
   @UiHandler("lineNumbers")
   void onLineNumbers(ValueChangeEvent<Boolean> e) {
     prefs.showLineNumbers(e.getValue());
-    view.setShowLineNumbers(prefs.showLineNumbers());
+    if (view != null) {
+      view.setShowLineNumbers(prefs.showLineNumbers());
+    }
   }
 
   @UiHandler("leftSide")
@@ -315,29 +366,35 @@
   @UiHandler("emptyPane")
   void onHideEmptyPane(ValueChangeEvent<Boolean> e) {
     prefs.hideEmptyPane(!e.getValue());
-    view.diffTable.setHideEmptyPane(prefs.hideEmptyPane());
-    if (prefs.hideEmptyPane()) {
-      if (view.diffTable.getChangeType() == ChangeType.ADDED) {
-        leftSide.setValue(false);
-        leftSide.setEnabled(false);
+    if (view != null) {
+      view.diffTable.setHideEmptyPane(prefs.hideEmptyPane());
+      if (prefs.hideEmptyPane()) {
+        if (view.diffTable.getChangeType() == ChangeType.ADDED) {
+          leftSide.setValue(false);
+          leftSide.setEnabled(false);
+        }
+      } else {
+        leftSide.setValue(view.diffTable.isVisibleA());
+        leftSide.setEnabled(true);
       }
-    } else {
-      leftSide.setValue(view.diffTable.isVisibleA());
-      leftSide.setEnabled(true);
     }
   }
 
   @UiHandler("topMenu")
   void onTopMenu(ValueChangeEvent<Boolean> e) {
     prefs.hideTopMenu(!e.getValue());
-    Gerrit.setHeaderVisible(!prefs.hideTopMenu());
-    view.resizeCodeMirror();
+    if (view != null) {
+      Gerrit.setHeaderVisible(!prefs.hideTopMenu());
+      view.resizeCodeMirror();
+    }
   }
 
   @UiHandler("autoHideDiffTableHeader")
   void onAutoHideDiffTableHeader(ValueChangeEvent<Boolean> e) {
     prefs.autoHideDiffTableHeader(!e.getValue());
-    view.setAutoHideDiffHeader(!e.getValue());
+    if (view != null) {
+      view.setAutoHideDiffHeader(!e.getValue());
+    }
   }
 
   @UiHandler("manualReview")
@@ -348,11 +405,13 @@
   @UiHandler("syntaxHighlighting")
   void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) {
     prefs.syntaxHighlighting(e.getValue());
-    mode.setEnabled(prefs.syntaxHighlighting());
-    if (prefs.syntaxHighlighting()) {
-      setMode(view.getContentType());
+    if (view != null) {
+      mode.setEnabled(prefs.syntaxHighlighting());
+      if (prefs.syntaxHighlighting()) {
+        setMode(view.getContentType());
+      }
+      view.setSyntaxHighlighting(prefs.syntaxHighlighting());
     }
-    view.setSyntaxHighlighting(prefs.syntaxHighlighting());
   }
 
   @UiHandler("mode")
@@ -386,42 +445,48 @@
   @UiHandler("whitespaceErrors")
   void onWhitespaceErrors(ValueChangeEvent<Boolean> e) {
     prefs.showWhitespaceErrors(e.getValue());
-    view.operation(new Runnable() {
-      @Override
-      public void run() {
-        boolean s = prefs.showWhitespaceErrors();
-        view.getCmFromSide(DisplaySide.A).setOption("showTrailingSpace", s);
-        view.getCmFromSide(DisplaySide.B).setOption("showTrailingSpace", s);
-      }
-    });
+    if (view != null) {
+      view.operation(new Runnable() {
+        @Override
+        public void run() {
+          boolean s = prefs.showWhitespaceErrors();
+          view.getCmFromSide(DisplaySide.A).setOption("showTrailingSpace", s);
+          view.getCmFromSide(DisplaySide.B).setOption("showTrailingSpace", s);
+        }
+      });
+    }
   }
 
   @UiHandler("renderEntireFile")
   void onRenderEntireFile(ValueChangeEvent<Boolean> e) {
     prefs.renderEntireFile(e.getValue());
-    view.updateRenderEntireFile();
+    if (view != null) {
+      view.updateRenderEntireFile();
+    }
   }
 
   @UiHandler("theme")
   void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
     final Theme newTheme = getSelectedTheme();
     prefs.theme(newTheme);
-    ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
-      @Override
-      public void onSuccess(Void result) {
-        view.operation(new Runnable() {
-          @Override
-          public void run() {
-            if (getSelectedTheme() == newTheme && isAttached()) {
-              String t = newTheme.name().toLowerCase();
-              view.getCmFromSide(DisplaySide.A).setOption("theme", t);
-              view.getCmFromSide(DisplaySide.B).setOption("theme", t);
-              view.setThemeStyles(newTheme.isDark());
+    if (view != null) {
+      ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          view.operation(new Runnable() {
+            @Override
+            public void run() {
+              if (getSelectedTheme() == newTheme && isAttached()) {
+                String t = newTheme.name().toLowerCase();
+                view.getCmFromSide(DisplaySide.A).setOption("theme", t);
+                view.getCmFromSide(DisplaySide.B).setOption("theme", t);
+                view.setThemeStyles(newTheme.isDark());
+              }
             }
-          }
-        });
-      }
-    });
+          });
+        }
+      });
+    }
   }
 
   private Theme getSelectedTheme() {
@@ -446,7 +511,9 @@
         Gerrit.setAccountDiffPreference(p);
       }
     });
-    close();
+    if (view != null) {
+      close();
+    }
   }
 
   @UiHandler("close")
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 e011091..da744c4 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
@@ -155,15 +155,17 @@
   </ui:style>
 
   <g:HTMLPanel styleName='{style.box}'>
-    <table style='width: 100%'>
-      <tr>
-        <td><ui:msg>Diff Preferences</ui:msg></td>
-        <td style='text-align: right'>
-          <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
-        </td>
-      </tr>
-    </table>
-    <hr/>
+    <div ui:field='header'>
+      <table style='width: 100%'>
+        <tr>
+          <td><ui:msg>Diff Preferences</ui:msg></td>
+          <td style='text-align: right'>
+            <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
+          </td>
+        </tr>
+      </table>
+      <hr/>
+    </div>
     <table class='{style.table}'>
       <tr>
         <th><ui:msg>Theme</ui:msg></th>
@@ -208,7 +210,7 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
-        <th><ui:msg>Language</ui:msg></th>
+        <th><div ui:field='modeLabel'><ui:msg>Language</ui:msg></div></th>
         <td><g:ListBox ui:field='mode'/></td>
       </tr>
       <tr>
@@ -240,7 +242,7 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
-        <th><ui:msg>Left Side</ui:msg></th>
+        <th><div ui:field='leftSideLabel'><ui:msg>Left Side</ui:msg></div></th>
         <td><g:ToggleButton ui:field='leftSide'>
           <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
           <g:downFace><ui:msg>Show</ui:msg></g:downFace>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java
new file mode 100644
index 0000000..d297f4d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.editor;
+
+import com.google.gerrit.client.account.EditPreferences;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+
+class EditPreferencesAction {
+  private final EditScreen view;
+  private final EditPreferences prefs;
+  private PopupPanel popup;
+  private EditPreferencesBox current;
+
+  EditPreferencesAction(EditScreen view, EditPreferences prefs) {
+    this.view = view;
+    this.prefs = prefs;
+  }
+
+  void show() {
+    if (popup != null) {
+      hide();
+      return;
+    }
+
+    current = new EditPreferencesBox(view);
+    current.set(prefs);
+
+    popup = new PopupPanel(true, false);
+    popup.setStyleName(current.style.dialog());
+    popup.add(current);
+    popup.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        view.getEditor().focus();
+        popup = null;
+        current = null;
+      }
+    });
+    popup.setPopupPositionAndShow(new PositionCallback() {
+      @Override
+      public void setPosition(int offsetWidth, int offsetHeight) {
+        popup.setPopupPosition(300, 120);
+      }
+    });
+  }
+
+  void hide() {
+    if (popup != null) {
+      popup.hide();
+      popup = null;
+      current = null;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
new file mode 100644
index 0000000..dadf5b2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
@@ -0,0 +1,314 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.editor;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.account.EditPreferences;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.NpIntTextBox;
+import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.ToggleButton;
+import com.google.gwt.user.client.ui.UIObject;
+
+import net.codemirror.theme.ThemeLoader;
+
+/** Displays current edit preferences. */
+public class EditPreferencesBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, EditPreferencesBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  public interface Style extends CssResource {
+    String dialog();
+  }
+
+  private final EditScreen view;
+  private EditPreferences prefs;
+
+  @UiField Style style;
+  @UiField Element header;
+  @UiField Anchor close;
+  @UiField NpIntTextBox tabWidth;
+  @UiField NpIntTextBox lineLength;
+  @UiField NpIntTextBox cursorBlinkRate;
+  @UiField ToggleButton topMenu;
+  @UiField ToggleButton syntaxHighlighting;
+  @UiField ToggleButton showTabs;
+  @UiField ToggleButton whitespaceErrors;
+  @UiField ToggleButton lineNumbers;
+  @UiField ToggleButton matchBrackets;
+  @UiField ToggleButton autoCloseBrackets;
+  @UiField ListBox theme;
+  @UiField ListBox keyMap;
+  @UiField Button apply;
+  @UiField Button save;
+
+  public EditPreferencesBox(EditScreen view) {
+    this.view = view;
+    initWidget(uiBinder.createAndBindUi(this));
+    initTheme();
+    initKeyMapType();
+
+    if (view == null) {
+      UIObject.setVisible(header, false);
+      apply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
+    }
+  }
+
+  public Style getStyle() {
+    return style;
+  }
+
+  public void set(EditPreferences prefs) {
+    this.prefs = prefs;
+
+    tabWidth.setIntValue(prefs.tabSize());
+    lineLength.setIntValue(prefs.lineLength());
+    cursorBlinkRate.setIntValue(prefs.cursorBlinkRate());
+    topMenu.setValue(!prefs.hideTopMenu());
+    syntaxHighlighting.setValue(prefs.syntaxHighlighting());
+    showTabs.setValue(prefs.showTabs());
+    whitespaceErrors.setValue(prefs.showWhitespaceErrors());
+    lineNumbers.setValue(prefs.hideLineNumbers());
+    matchBrackets.setValue(prefs.matchBrackets());
+    autoCloseBrackets.setValue(prefs.autoCloseBrackets());
+    setTheme(prefs.theme());
+    setKeyMapType(prefs.keyMapType());
+  }
+
+  @UiHandler("tabWidth")
+  void onTabWidth(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      prefs.tabSize(Math.max(1, Integer.parseInt(v)));
+      if (view != null) {
+        view.getEditor().setOption("tabSize", v);
+      }
+    }
+  }
+
+  @UiHandler("lineLength")
+  void onLineLength(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      prefs.lineLength(Math.max(1, Integer.parseInt(v)));
+      if (view != null) {
+        view.setLineLength(prefs.lineLength());
+      }
+    }
+  }
+
+  @UiHandler("cursorBlinkRate")
+  void onCursoBlinkRate(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      // A negative value hides the cursor entirely:
+      // don't let user shoot himself in the foot.
+      prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
+      if (view != null) {
+        view.getEditor().setOption("cursorBlinkRate", prefs.cursorBlinkRate());
+      }
+    }
+  }
+
+  @UiHandler("topMenu")
+  void onTopMenu(ValueChangeEvent<Boolean> e) {
+    prefs.hideTopMenu(!e.getValue());
+    if (view != null) {
+      Gerrit.setHeaderVisible(!prefs.hideTopMenu());
+      view.resizeCodeMirror();
+    }
+  }
+
+  @UiHandler("showTabs")
+  void onShowTabs(ValueChangeEvent<Boolean> e) {
+    prefs.showTabs(e.getValue());
+    if (view != null) {
+      view.setShowTabs(prefs.showTabs());
+    }
+  }
+
+  @UiHandler("whitespaceErrors")
+  void onshowTrailingSpace(ValueChangeEvent<Boolean> e) {
+    prefs.showWhitespaceErrors(e.getValue());
+    if (view != null) {
+      view.setShowWhitespaceErrors(prefs.showWhitespaceErrors());
+    }
+  }
+
+  @UiHandler("lineNumbers")
+  void onLineNumbers(ValueChangeEvent<Boolean> e) {
+    prefs.hideLineNumbers(e.getValue());
+    if (view != null) {
+      view.setShowLineNumbers(prefs.hideLineNumbers());
+    }
+  }
+
+  @UiHandler("syntaxHighlighting")
+  void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) {
+    prefs.syntaxHighlighting(e.getValue());
+    if (view != null) {
+      view.setSyntaxHighlighting(prefs.syntaxHighlighting());
+    }
+  }
+
+  @UiHandler("matchBrackets")
+  void onMatchBrackets(ValueChangeEvent<Boolean> e) {
+    prefs.matchBrackets(e.getValue());
+    if (view != null) {
+      view.getEditor().setOption("matchBrackets", prefs.matchBrackets());
+    }
+  }
+
+  @UiHandler("autoCloseBrackets")
+  void onCloseBrackets(ValueChangeEvent<Boolean> e) {
+    prefs.autoCloseBrackets(e.getValue());
+    if (view != null) {
+      view.getEditor().setOption("autoCloseBrackets", prefs.autoCloseBrackets());
+    }
+  }
+
+  @UiHandler("theme")
+  void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
+    final Theme newTheme = Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
+    prefs.theme(newTheme);
+    if (view != null) {
+      ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          view.getEditor().operation(new Runnable() {
+            @Override
+            public void run() {
+              String t = newTheme.name().toLowerCase();
+              view.getEditor().setOption("theme", t);
+            }
+          });
+        }
+      });
+    }
+  }
+
+  @UiHandler("keyMap")
+  void onKeyMap(@SuppressWarnings("unused") ChangeEvent e) {
+    KeyMapType keyMapType = KeyMapType.valueOf(
+        keyMap.getValue(keyMap.getSelectedIndex()));
+    prefs.keyMapType(keyMapType);
+    if (view != null) {
+      view.getEditor().setOption("keyMap", keyMapType.name().toLowerCase());
+    }
+  }
+
+  @UiHandler("apply")
+  void onApply(@SuppressWarnings("unused") ClickEvent e) {
+    close();
+  }
+
+  @UiHandler("save")
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
+    AccountApi.putEditPreferences(prefs, new GerritCallback<VoidResult>() {
+      @Override
+      public void onSuccess(VoidResult n) {
+        prefs.copyTo(Gerrit.getEditPreferences());
+      }
+    });
+    close();
+  }
+
+  @UiHandler("close")
+  void onClose(ClickEvent e) {
+    e.preventDefault();
+    close();
+  }
+
+  private void close() {
+    ((PopupPanel) getParent()).hide();
+  }
+
+  private void setTheme(Theme v) {
+    String name = v != null ? v.name() : Theme.DEFAULT.name();
+    for (int i = 0; i < theme.getItemCount(); i++) {
+      if (theme.getValue(i).equals(name)) {
+        theme.setSelectedIndex(i);
+        return;
+      }
+    }
+    theme.setSelectedIndex(0);
+  }
+
+  private void initTheme() {
+    theme.addItem(
+        Theme.DEFAULT.name().toLowerCase(),
+        Theme.DEFAULT.name());
+    theme.addItem(
+        Theme.ECLIPSE.name().toLowerCase(),
+        Theme.ECLIPSE.name());
+    theme.addItem(
+        Theme.ELEGANT.name().toLowerCase(),
+        Theme.ELEGANT.name());
+    theme.addItem(
+        Theme.NEAT.name().toLowerCase(),
+        Theme.NEAT.name());
+    theme.addItem(
+        Theme.MIDNIGHT.name().toLowerCase(),
+        Theme.MIDNIGHT.name());
+    theme.addItem(
+        Theme.NIGHT.name().toLowerCase(),
+        Theme.NIGHT.name());
+    theme.addItem(
+        Theme.TWILIGHT.name().toLowerCase(),
+        Theme.TWILIGHT.name());
+  }
+
+  private void setKeyMapType(KeyMapType v) {
+    String name = v != null ? v.name() : KeyMapType.DEFAULT.name();
+    for (int i = 0; i < keyMap.getItemCount(); i++) {
+      if (keyMap.getValue(i).equals(name)) {
+        keyMap.setSelectedIndex(i);
+        return;
+      }
+    }
+    keyMap.setSelectedIndex(0);
+  }
+
+  private void initKeyMapType() {
+    keyMap.addItem(
+        KeyMapType.DEFAULT.name().toLowerCase(),
+        KeyMapType.DEFAULT.name());
+    keyMap.addItem(
+        KeyMapType.EMACS.name().toLowerCase(),
+        KeyMapType.EMACS.name());
+    keyMap.addItem(
+        KeyMapType.VIM.name().toLowerCase(),
+        KeyMapType.VIM.name());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
new file mode 100644
index 0000000..ec8ad39
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:x='urn:import:com.google.gerrit.client.ui'>
+  <ui:style type='com.google.gerrit.client.editor.EditPreferencesBox.Style'>
+    @external .gwt-TextBox;
+    @external .gwt-ToggleButton .html-face;
+    @external .gwt-ToggleButton-up;
+    @external .gwt-ToggleButton-up-hovering;
+    @external .gwt-ToggleButton-up-disabled;
+    @external .gwt-ToggleButton-down;
+    @external .gwt-ToggleButton-down-hovering;
+    @external .gwt-ToggleButton-down-disabled;
+
+    .dialog {
+      background: rgba(0, 0, 0, 0.85) none repeat scroll 0 50%;
+      color: #ffffff;
+      font-family: arial,sans-serif;
+      font-weight: bold;
+      overflow: hidden;
+      text-align: left;
+      text-shadow: 1px 1px 7px #000000;
+      min-width: 300px;
+      z-index: 200;
+      border-radius: 10px;
+    }
+
+    @if user.agent safari {
+      .dialog {
+        \-webkit-border-radius: 10px;
+      }
+    }
+
+    @if user.agent gecko1_8 {
+      .dialog {
+        \-moz-border-radius: 10px;
+      }
+    }
+
+    .box { margin: 10px; }
+    .box .gwt-TextBox { padding: 0; }
+    .context { vertical-align: bottom; }
+
+    .table tr { min-height: 23px; }
+    .table th,
+    .table td {
+      white-space: nowrap;
+      color: #ffffff;
+    }
+    .table th {
+      padding-right: 8px;
+      text-align: right;
+    }
+
+    .box a,
+    .box a:visited,
+    .box a:hover {
+      color: #dddd00;
+    }
+
+    .box .gwt-ToggleButton {
+      position: relative;
+      height: 19px;
+      width: 140px;
+      background: #fff;
+      color: #000;
+      text-shadow: none;
+    }
+    .box .gwt-ToggleButton .html-face {
+      position: absolute;
+      top: 0;
+      width: 68px;
+      height: 17px;
+      line-height: 17px;
+      text-align: center;
+      border-width: 1px;
+    }
+
+    .box .gwt-ToggleButton-up,
+    .box .gwt-ToggleButton-up-hovering,
+    .box .gwt-ToggleButton-up-disabled,
+    .box .gwt-ToggleButton-down,
+    .box .gwt-ToggleButton-down-hovering,
+    .box .gwt-ToggleButton-down-disabled {
+      padding: 0;
+      border: 0;
+    }
+    .box .gwt-ToggleButton-up .html-face,
+    .box .gwt-ToggleButton-up-hovering .html-face {
+      left: 0;
+      background: #cacaca;
+      border-style: outset;
+    }
+    .box .gwt-ToggleButton-down .html-face,
+    .box .gwt-ToggleButton-down-hovering .html-face {
+      right: 0;
+      background: #bcf;
+      border-style: inset;
+    }
+
+    .box button {
+      margin: 6px 3px 0 0;
+      border-color: rgba(0, 0, 0, 0.1);
+      text-align: center;
+      font-size: 8pt;
+      font-weight: bold;
+      border: 1px solid;
+      cursor: pointer;
+      color: #444;
+      background-color: #f5f5f5;
+      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+      -webkit-border-radius: 2px;
+      -webkit-box-sizing: content-box;
+    }
+    .box button div {
+      color: #444;
+      height: 10px;
+      min-width: 54px;
+      line-height: 10px;
+      white-space: nowrap;
+    }
+
+    button.apply {
+      background-color: #4d90fe;
+      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+    }
+    button.apply div { color: #fff; }
+
+    button.save {
+      margin-left: 10px;
+      color: #d14836;
+      background-color: #d14836;
+      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
+    }
+    button.save div { color: #fff; }
+  </ui:style>
+
+  <g:HTMLPanel styleName='{style.box}'>
+    <div ui:field='header'>
+      <table style='width: 100%'>
+        <tr>
+          <td><ui:msg>Edit Preferences</ui:msg></td>
+          <td style='text-align: right'>
+            <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
+          </td>
+        </tr>
+      </table>
+      <hr/>
+    </div>
+    <table class='{style.table}'>
+      <tr>
+        <th><ui:msg>Theme</ui:msg></th>
+        <td><g:ListBox ui:field='theme'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Key Map</ui:msg></th>
+        <td><g:ListBox ui:field='keyMap'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Tab Width</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='tabWidth'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Columns</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='lineLength'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Cursor Blink Rate</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='cursorBlinkRate'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Top Menu</ui:msg></th>
+        <td><g:ToggleButton ui:field='topMenu'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Syntax Highlighting</ui:msg></th>
+        <td><g:ToggleButton ui:field='syntaxHighlighting'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Show Tabs</ui:msg></th>
+        <td><g:ToggleButton ui:field='showTabs'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+      <th><ui:msg>Whitespace Errors</ui:msg></th>
+        <td><g:ToggleButton ui:field='whitespaceErrors'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Line Numbers</ui:msg></th>
+        <td><g:ToggleButton ui:field='lineNumbers'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Match Brackets</ui:msg></th>
+        <td><g:ToggleButton ui:field='matchBrackets'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Auto Close Brackets</ui:msg></th>
+        <td><g:ToggleButton ui:field='autoCloseBrackets'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <td></td>
+        <td>
+          <g:Button ui:field='apply' styleName='{style.apply}'>
+            <div><ui:msg>Apply</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='save' styleName='{style.save}'>
+            <div><ui:msg>Save</ui:msg></div>
+          </g:Button>
+        </td>
+      </tr>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index e5a5b53..a546c62 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.JumpKeys;
 import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.account.DiffPreferences;
+import com.google.gerrit.client.account.EditPreferences;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeEditApi;
 import com.google.gerrit.client.diff.DiffApi;
@@ -42,6 +42,7 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.KeyMapType;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -87,7 +88,8 @@
   private final PatchSet.Id revision;
   private final String path;
   private final int startLine;
-  private DiffPreferences prefs;
+  private EditPreferences prefs;
+  private EditPreferencesAction editPrefsAction;
   private CodeMirror cm;
   private HttpResponse<NativeString> content;
   private EditFileInfo editFileInfo;
@@ -113,7 +115,7 @@
     this.revision = patch.getParentKey();
     this.path = patch.get();
     this.startLine = startLine - 1;
-    prefs = DiffPreferences.create(Gerrit.getAccountDiffPreference());
+    setRequiresSignIn(true);
     add(uiBinder.createAndBindUi(this));
     addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
   }
@@ -129,6 +131,8 @@
   protected void onLoad() {
     super.onLoad();
 
+    prefs = EditPreferences.create(Gerrit.getEditPreferences());
+
     CallbackGroup group1 = new CallbackGroup();
     final CallbackGroup group2 = new CallbackGroup();
     final CallbackGroup group3 = new CallbackGroup();
@@ -163,7 +167,6 @@
           }
         }));
 
-
     if (revision.get() == 0) {
       ChangeEditApi.getMeta(revision, path,
           group1.add(new AsyncCallback<EditFileInfo>() {
@@ -224,7 +227,6 @@
       @Override
       protected void preDisplay(Void result) {
         initEditor(content);
-        content = null;
 
         renderLinks(editFileInfo, diffLinks);
         editFileInfo = null;
@@ -237,11 +239,18 @@
   @Override
   public void registerKeys() {
     super.registerKeys();
-    cm.addKeyMap(KeyMap.create()
+    KeyMap localKeyMap = KeyMap.create();
+    localKeyMap
         .on("Ctrl-L", gotoLine())
         .on("Cmd-L", gotoLine())
-        .on("Cmd-S", save())
-        .on("Ctrl-S", save()));
+        .on("Cmd-S", save());
+
+    // TODO(davido): Find a better way to prevent key maps collisions
+    if (prefs.keyMapType() != KeyMapType.EMACS) {
+      localKeyMap.on("Ctrl-S", save());
+    }
+
+    cm.addKeyMap(localKeyMap);
   }
 
   private Runnable gotoLine() {
@@ -300,9 +309,8 @@
 
     cm.adjustHeight(header.getOffsetHeight());
     cm.on("cursorActivity", updateCursorPosition());
-    cm.extras().showTabs(prefs.showTabs());
-    cm.extras().lineLength(
-        Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
+    setShowTabs(prefs.showTabs());
+    setLineLength(prefs.lineLength());
     cm.refresh();
     cm.focus();
 
@@ -310,6 +318,7 @@
       cm.scrollToLine(startLine);
     }
     updateActiveLine();
+    editPrefsAction = new EditPreferencesAction(this, prefs);
   }
 
   @Override
@@ -329,6 +338,15 @@
     JumpKeys.enable(true);
   }
 
+  CodeMirror getEditor() {
+    return cm;
+  }
+
+  @UiHandler("editSettings")
+  void onEditSetting(@SuppressWarnings("unused") ClickEvent e) {
+    editPrefsAction.show();
+  }
+
   @UiHandler("save")
   void onSave(@SuppressWarnings("unused") ClickEvent e) {
     save().run();
@@ -342,6 +360,52 @@
     }
   }
 
+  void setLineLength(int length) {
+    cm.extras().lineLength(
+        Patch.COMMIT_MSG.equals(path) ? 72 : length);
+  }
+
+  void setShowLineNumbers(boolean show) {
+    cm.setOption("lineNumbers", show);
+  }
+
+  void setShowWhitespaceErrors(final boolean show) {
+    cm.operation(new Runnable() {
+      @Override
+      public void run() {
+        cm.setOption("showTrailingSpace", show);
+      }
+    });
+  }
+
+  void setShowTabs(boolean show) {
+    cm.extras().showTabs(show);
+  }
+
+  void resizeCodeMirror() {
+    cm.adjustHeight(header.getOffsetHeight());
+  }
+
+  void setSyntaxHighlighting(boolean b) {
+    ModeInfo modeInfo = ModeInfo.findMode(content.getContentType(), path);
+    final String mode = modeInfo != null ? modeInfo.mode() : null;
+    if (b && mode != null && !mode.isEmpty()) {
+      injectMode(mode, new AsyncCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          cm.setOption("mode", mode);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          prefs.syntaxHighlighting(false);
+        }
+      });
+    } else {
+      cm.setOption("mode", (String) null);
+    }
+  }
+
   private void upToChange() {
     Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
   }
@@ -358,15 +422,17 @@
     cm = CodeMirror.create(editor, Configuration.create()
         .set("value", content)
         .set("readOnly", false)
-        .set("cursorBlinkRate", 0)
+        .set("cursorBlinkRate", prefs.cursorBlinkRate())
         .set("cursorHeight", 0.85)
-        .set("lineNumbers", true)
+        .set("lineNumbers", prefs.hideLineNumbers())
         .set("tabSize", prefs.tabSize())
         .set("lineWrapping", false)
+        .set("matchBrackets", prefs.matchBrackets())
+        .set("autoCloseBrackets", prefs.autoCloseBrackets())
         .set("scrollbarStyle", "overlay")
         .set("styleSelectedText", true)
-        .set("showTrailingSpace", true)
-        .set("keyMap", "default")
+        .set("showTrailingSpace", prefs.showWhitespaceErrors())
+        .set("keyMap", prefs.keyMapType().name().toLowerCase())
         .set("theme", prefs.theme().name().toLowerCase())
         .set("mode", mode != null ? mode.mode() : null));
   }
@@ -452,6 +518,13 @@
         if (!cm.isClean(generation)) {
           close.setEnabled(false);
           String text = cm.getValue();
+          if (Patch.COMMIT_MSG.equals(path)) {
+            String trimmed = text.trim() + "\r";
+            if (!trimmed.equals(text)) {
+              text = trimmed;
+              cm.setValue(text);
+            }
+          }
           final int g = cm.changeGeneration(false);
           ChangeEditApi.put(revision.getParentKey().get(), path, text,
               new GerritCallback<VoidResult>() {
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 a82dc49..88af398 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
@@ -16,6 +16,7 @@
 -->
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:style gss='false'>
     @external .CodeMirror, .CodeMirror-cursor;
 
@@ -115,6 +116,13 @@
       padding-top: 2px;
       padding-right: 3px;
     }
+
+    .preferences {
+      position: relative;
+      top: 2px;
+      cursor: pointer;
+      outline: none;
+    }
   </ui:style>
   <g:HTMLPanel styleName='{style.header}'>
     <div class='{style.headerLine}' ui:field='header'>
@@ -135,6 +143,13 @@
        <span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
        <div class='{style.navigation}'>
          <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
+         <g:Image
+             ui:field='editSettings'
+             styleName='{style.preferences}'
+             resource='{ico.gear}'
+             title='Edit screen preferences'>
+            <ui:attribute name='title'/>
+         </g:Image>
        </div>
     </div>
     <div ui:field='editor' />
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 3ec3d28..0987e83 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
@@ -1258,6 +1258,10 @@
   cursor: pointer;
 }
 
+.branchTableDeleteButton {
+  margin-top: 5px;
+}
+
 .branchTablePrevNextLinks {
   position: relative;
 }
@@ -1310,3 +1314,10 @@
   font-size: 7pt;
   padding: 1px;
 }
+
+/* List Screens */
+.pagingLink {
+  font-size: 18px;
+  margin-top: 5px;
+  margin-bottom: 15px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
index b94f5d5..8d166a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -17,17 +17,9 @@
 import com.google.gerrit.client.info.ActionInfo;
 import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.reviewdb.client.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 RefNames.shortName(ref());
-  }
-
-  public final native String ref() /*-{ return this.ref; }-*/;
-  public final native String revision() /*-{ return this.revision; }-*/;
+public class BranchInfo extends RefInfo {
   public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions }-*/;
   public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
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 3f3875a..322a354 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -53,6 +53,9 @@
   public final native InheritedBooleanInfo enableSignedPush()
   /*-{ return this.enable_signed_push; }-*/;
 
+  public final native InheritedBooleanInfo requireSignedPush()
+  /*-{ return this.require_signed_push; }-*/;
+
   public final SubmitType submitType() {
     return SubmitType.valueOf(submitTypeRaw());
   }
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 53eba42..fffdd3f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -44,6 +44,32 @@
         .put(input, cb);
   }
 
+  private static RestApi getRestApi(Project.NameKey name, String viewName,
+      int limit, int start, String match) {
+    RestApi call = project(name).view(viewName);
+    call.addParameter("n", limit);
+    call.addParameter("s", start);
+    if (match != null) {
+      if (match.startsWith("^")) {
+        call.addParameter("r", match);
+      } else {
+        call.addParameter("m", match);
+      }
+    }
+    return call;
+  }
+
+  /** Retrieve all visible tags of the project */
+  public static void getTags(Project.NameKey name,
+      AsyncCallback<JsArray<TagInfo>> cb) {
+    project(name).view("tags").get(cb);
+  }
+
+  public static void getTags(Project.NameKey name, int limit, int start,
+      String match, AsyncCallback<JsArray<TagInfo>> cb) {
+    getRestApi(name, "tags", limit, start, match).get(cb);
+  }
+
   /** Create a new branch */
   public static void createBranch(Project.NameKey name, String ref,
       String revision, AsyncCallback<BranchInfo> cb) {
@@ -60,17 +86,7 @@
 
   public static void getBranches(Project.NameKey name, int limit, int start,
        String match, AsyncCallback<JsArray<BranchInfo>> cb) {
-    RestApi call = project(name).view("branches");
-    call.addParameter("n", limit);
-    call.addParameter("s", start);
-    if (match != null) {
-      if (match.startsWith("^")) {
-        call.addParameter("r", match);
-      } else {
-        call.addParameter("m", match);
-      }
-    }
-    call.get(cb);
+    getRestApi(name, "branches", limit, start, match).get(cb);
   }
 
   /**
@@ -101,6 +117,7 @@
       InheritableBoolean createNewChangeForAllNotInTarget,
       InheritableBoolean requireChangeId,
       InheritableBoolean enableSignedPush,
+      InheritableBoolean requireSignedPush,
       String maxObjectSizeLimit,
       SubmitType submitType, ProjectState state,
       Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
@@ -115,6 +132,9 @@
     if (enableSignedPush != null) {
       in.setEnableSignedPush(enableSignedPush);
     }
+    if (requireSignedPush != null) {
+      in.setRequireSignedPush(requireSignedPush);
+    }
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -241,6 +261,12 @@
     private final native void setEnableSignedPushRaw(String v)
     /*-{ if(v)this.enable_signed_push=v; }-*/;
 
+    final void setRequireSignedPush(InheritableBoolean v) {
+      setRequireSignedPushRaw(v.name());
+    }
+    private final native void setRequireSignedPushRaw(String v)
+    /*-{ if(v)this.require_signed_push=v; }-*/;
+
     final native void setMaxObjectSizeLimit(String l)
     /*-{ if(l)this.max_object_size_limit=l; }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
new file mode 100644
index 0000000..053dbd3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class RefInfo extends JavaScriptObject {
+  public final String getShortName() {
+    return RefNames.shortName(ref());
+  }
+
+  public final native String ref() /*-{ return this.ref; }-*/;
+  public final native String revision() /*-{ return this.revision; }-*/;
+
+  protected RefInfo() {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
similarity index 65%
copy from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
index cd01186..ee1d1af 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.client.projects;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
+public class TagInfo extends RefInfo {
+
+  // TODO(dpursehouse) add extra tag-related fields (message, tagger, etc)
+  protected TagInfo() {
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index e48477f..771423e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -72,7 +72,8 @@
       }
       return sce.getStatusCode() == Response.SC_FORBIDDEN
           && (sce.getEncodedResponse().equals("Authentication required")
-              || sce.getEncodedResponse().startsWith("Must be signed-in"));
+              || sce.getEncodedResponse().startsWith("Must be signed-in")
+              || sce.getEncodedResponse().startsWith("Invalid authentication"));
     }
     return false;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java
new file mode 100644
index 0000000..e4ad903
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.admin.Util;
+
+public class PagingHyperlink extends Hyperlink {
+
+  public static PagingHyperlink createPrev() {
+    return new PagingHyperlink(Util.C.pagedListPrev());
+  }
+
+  public static PagingHyperlink createNext() {
+    return new PagingHyperlink(Util.C.pagedListNext());
+  }
+
+  private PagingHyperlink(String text) {
+    super(text, true, "");
+    setStyleName(Gerrit.RESOURCES.css().pagingLink());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
index 782500d..85b350d 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -75,6 +75,7 @@
       Modes.I.stex(),
       Modes.I.velocity(),
       Modes.I.verilog(),
+      Modes.I.vhdl(),
       Modes.I.xml(),
       Modes.I.yaml(),
     });
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
index 8170c2b..42990b8 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -61,6 +61,7 @@
   @Source("tcl.js") @DoNotEmbed DataResource tcl();
   @Source("velocity.js") @DoNotEmbed DataResource velocity();
   @Source("verilog.js") @DoNotEmbed DataResource verilog();
+  @Source("vhdl.js") @DoNotEmbed DataResource vhdl();
   @Source("xml.js") @DoNotEmbed DataResource xml();
   @Source("yaml.js") @DoNotEmbed DataResource yaml();
 
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java
new file mode 100644
index 0000000..0f659c9
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import static com.google.gerrit.client.FormatUtil.formatBytes;
+import static org.junit.Assert.assertEquals;
+
+import com.googlecode.gwt.test.GwtModule;
+import com.googlecode.gwt.test.GwtTest;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+@GwtModule("com.google.gerrit.GerritGwtUI")
+@Ignore
+public class FormatUtilTest extends GwtTest {
+  @Test
+  public void testFormatBytes() {
+    assertEquals("+/- 0 B", formatBytes(0));
+    assertEquals("+27 B", formatBytes(27));
+    assertEquals("+999 B", formatBytes(999));
+    assertEquals("+1000 B", formatBytes(1000));
+    assertEquals("+1023 B", formatBytes(1023));
+    assertEquals("+1.0 KiB", formatBytes(1024));
+    assertEquals("+1.7 KiB", formatBytes(1728));
+    assertEquals("+108.0 KiB", formatBytes(110592));
+    assertEquals("+6.8 MiB", formatBytes(7077888));
+    assertEquals("+432.0 MiB", formatBytes(452984832));
+    assertEquals("+27.0 GiB", formatBytes(28991029248L));
+    assertEquals("+1.7 TiB", formatBytes(1855425871872L));
+    assertEquals("+8.0 EiB", formatBytes(9223372036854775807L));
+
+    assertEquals("-27 B", formatBytes(-27));
+    assertEquals("-1.7 MiB", formatBytes(-1728));
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index d315fa3..a6e4b44 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -136,7 +136,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     if (user == null) {
       if (isSignedIn()) {
         user = identified.create(val.getAccountId());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
index 94b8f29..019efb4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
@@ -72,7 +72,7 @@
       throws IOException, ServletException {
     CurrentUser user = userProvider.get();
     if (user != null && user.isIdentifiedUser()) {
-      IdentifiedUser who = (IdentifiedUser) user;
+      IdentifiedUser who = user.asIdentifiedUser();
       if (who.getUserName() != null && !who.getUserName().isEmpty()) {
         req.setAttribute(REQ_ATTR_KEY, who.getUserName());
       } else {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 43cd741..89d62dd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -63,8 +63,6 @@
 import org.eclipse.jgit.transport.resolver.UploadPackFactory;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -158,13 +156,6 @@
     public Repository open(HttpServletRequest req, String projectName)
         throws RepositoryNotFoundException, ServiceNotAuthorizedException,
         ServiceNotEnabledException {
-      try {
-        // TODO: remove this code when Guice fixes its issue 745
-        projectName = URLDecoder.decode(projectName, "UTF-8");
-      } catch (UnsupportedEncodingException e) {
-        // leave it encoded
-      }
-
       while (projectName.endsWith("/")) {
         projectName = projectName.substring(0, projectName.length() - 1);
       }
@@ -186,7 +177,7 @@
         throw new RepositoryNotFoundException(projectName);
       }
 
-      CurrentUser user = pc.getCurrentUser();
+      CurrentUser user = pc.getUser();
       user.setAccessPath(AccessPath.GIT);
 
       if (!pc.isVisible()) {
@@ -301,12 +292,12 @@
         throws ServiceNotAuthorizedException {
       final ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
 
-      if (!(pc.getCurrentUser().isIdentifiedUser())) {
+      if (!(pc.getUser().isIdentifiedUser())) {
         // Anonymous users are not permitted to push.
         throw new ServiceNotAuthorizedException();
       }
 
-      final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser();
+      final IdentifiedUser user = pc.getUser().asIdentifiedUser();
       final ReceiveCommits rc = factory.create(pc, db).getReceiveCommits();
       ReceivePack rp = rc.getReceivePack();
       rp.setRefLogIdent(user.newRefLogIdent());
@@ -376,14 +367,13 @@
         return;
       }
 
-      if (!(pc.getCurrentUser().isIdentifiedUser())) {
+      if (!(pc.getUser().isIdentifiedUser())) {
         chain.doFilter(request, response);
         return;
       }
 
       AdvertisedObjectsCacheKey cacheKey = AdvertisedObjectsCacheKey.create(
-          ((IdentifiedUser) pc.getCurrentUser()).getAccountId(),
-          projectName);
+          pc.getUser().getAccountId(), projectName);
 
       if (isGet) {
         cache.invalidate(cacheKey);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
index 1eb88b1..95f4536 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.io.ByteStreams;
 
 import org.w3c.dom.Document;
@@ -27,7 +29,6 @@
 import java.io.InputStream;
 import java.io.StringWriter;
 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;
@@ -50,7 +51,7 @@
 /** Utility functions to deal with HTML using W3C DOM operations. */
 public class HtmlDomUtil {
   /** Standard character encoding we prefer (UTF-8). */
-  public static final Charset ENC = StandardCharsets.UTF_8;
+  public static final Charset ENC = UTF_8;
 
   /** DOCTYPE for a standards mode HTML document. */
   public static final String HTML_STRICT =
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index ada3ebf..88584eb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -78,7 +78,7 @@
       final HttpServletResponse rsp) throws IOException {
 
     final String sid = webSession.get().getSessionId();
-    final CurrentUser currentUser = webSession.get().getCurrentUser();
+    final CurrentUser currentUser = webSession.get().getUser();
     final String what = "sign out";
     final long when = TimeUtil.nowMs();
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index 47593aa..adad03f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -34,8 +34,8 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
-    return session.get().getCurrentUser();
+  public CurrentUser getUser() {
+    return session.get().getUser();
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
index 6c17c87..177ff04 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
@@ -17,7 +17,6 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.restapi.Url;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -25,11 +24,11 @@
   private static final String DEFAULT_TOKEN = '#' + PageLinks.MINE;
 
   public static String getToken(final HttpServletRequest req){
-    String encodedToken = req.getPathInfo();
-    if (Strings.isNullOrEmpty(encodedToken)) {
+    String token = req.getPathInfo();
+    if (Strings.isNullOrEmpty(token)) {
       return DEFAULT_TOKEN;
     } else {
-      return CharMatcher.is('/').trimLeadingFrom(Url.decode(encodedToken));
+      return CharMatcher.is('/').trimLeadingFrom(token);
     }
   }
 
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 5d896df..6e6324e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.common.base.MoreObjects;
@@ -188,7 +189,7 @@
   }
 
   private String encoding(HttpServletRequest req) {
-    return MoreObjects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
+    return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
   }
 
   static class Response extends HttpServletResponseWrapper {
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 33b9fed..38dd118 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
@@ -36,7 +37,6 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Collections;
@@ -189,25 +189,17 @@
   }
 
   private static String H(String data) {
-    try {
-      MessageDigest md = newMD5();
-      md.update(data.getBytes("UTF-8"));
-      return LHEX(md.digest());
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("UTF-8 encoding not available", e);
-    }
+    MessageDigest md = newMD5();
+    md.update(data.getBytes(UTF_8));
+    return LHEX(md.digest());
   }
 
   private static String KD(String secret, String data) {
-    try {
-      MessageDigest md = newMD5();
-      md.update(secret.getBytes("UTF-8"));
-      md.update((byte) ':');
-      md.update(data.getBytes("UTF-8"));
-      return LHEX(md.digest());
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("UTF-8 encoding not available", e);
-    }
+    MessageDigest md = newMD5();
+    md.update(secret.getBytes(UTF_8));
+    md.update((byte) ':');
+    md.update(data.getBytes(UTF_8));
+    return LHEX(md.digest());
   }
 
   private static MessageDigest newMD5() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 614184a..f6b79e5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -84,7 +84,7 @@
         return;
       }
 
-      CurrentUser self = session.get().getCurrentUser();
+      CurrentUser self = session.get().getUser();
       if (!self.getCapabilities().canRunAs()) {
         replyError(req, res,
             SC_FORBIDDEN,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
index 3349cc1..b2d32fc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -25,7 +25,7 @@
   public String getXGerritAuth();
   public boolean isValidXGerritAuth(String keyIn);
   public AccountExternalId.Key getLastLoginExternalId();
-  public CurrentUser getCurrentUser();
+  public CurrentUser getUser();
   public void login(AuthResult res, boolean rememberMe);
 
   /** Set the user account for this current request only. */
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index ccc945f..bfbf1ff 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.auth.container;
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -102,7 +103,7 @@
       final byte[] bin = HtmlDomUtil.toUTF8(doc);
       rsp.setStatus(HttpServletResponse.SC_FORBIDDEN);
       rsp.setContentType("text/html");
-      rsp.setCharacterEncoding("UTF-8");
+      rsp.setCharacterEncoding(UTF_8.name());
       rsp.setContentLength(bin.length);
       try (ServletOutputStream out = rsp.getOutputStream()) {
         out.write(bin);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index 5388048..a5b4f7a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.ldap;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
@@ -90,7 +92,7 @@
     byte[] bin = HtmlDomUtil.toUTF8(doc);
     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     res.setContentType("text/html");
-    res.setCharacterEncoding("UTF-8");
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(bin.length);
     try (ServletOutputStream out = res.getOutputStream()) {
       out.write(bin);
@@ -106,6 +108,7 @@
   @Override
   protected void doPost(HttpServletRequest req, HttpServletResponse res)
       throws ServletException, IOException {
+    req.setCharacterEncoding(UTF_8.name());
     String username = Strings.nullToEmpty(req.getParameter("username")).trim();
     String password = Strings.nullToEmpty(req.getParameter("password"));
     String remember = Strings.nullToEmpty(req.getParameter("rememberme"));
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 75e468e..abcc4fe 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.gitweb;
 
 import static com.google.gerrit.common.FileUtil.lastModified;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.GitwebCgiConfig;
@@ -51,8 +52,6 @@
     }
   }
 
-  private static final String ENC = "UTF-8";
-
   private final long modified;
   private final byte[] raw_css;
   private final byte[] gz_css;
@@ -65,7 +64,7 @@
       final String raw = HtmlDomUtil.readFile(dir, name);
       if (raw != null) {
         modified = lastModified(src);
-        raw_css = raw.getBytes(ENC);
+        raw_css = raw.getBytes(UTF_8);
         gz_css = HtmlDomUtil.compress(raw_css);
       } else {
         modified = -1L;
@@ -89,7 +88,7 @@
       final HttpServletResponse rsp) throws IOException {
     if (raw_css != null) {
       rsp.setContentType("text/css");
-      rsp.setCharacterEncoding(ENC);
+      rsp.setCharacterEncoding(UTF_8.name());
       final byte[] toSend;
       if (RPCServletUtils.acceptsGzipEncoding(req)) {
         rsp.setHeader("Content-Encoding", "gzip");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebModule.java
index e73cb11..21a4d65 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebModule.java
@@ -19,7 +19,7 @@
 public class GitwebModule extends ServletModule {
   @Override
   protected void configureServlets() {
-    serve("/gitweb").with(GitwebServlet.class);
+    serveRegex("^/(?:a/)?gitweb").with(GitwebServlet.class);
     serve("/gitweb-logo.png").with(GitLogoServlet.class);
     serve("/gitweb.js").with(GitwebJavaScriptServlet.class);
     serve("/gitweb-default.css").with(GitwebCssServlet.Default.class);
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 e5af965..9a85562 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,7 @@
 
 package com.google.gerrit.httpd.gitweb;
 
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.PageLinks;
@@ -550,8 +551,8 @@
     }
 
     String remoteUser = null;
-    if (project.getCurrentUser().isIdentifiedUser()) {
-      final IdentifiedUser u = (IdentifiedUser) project.getCurrentUser();
+    if (project.getUser().isIdentifiedUser()) {
+      final IdentifiedUser u = project.getUser().asIdentifiedUser();
       final String user = u.getUserName();
       env.set("GERRIT_USER_NAME", user);
       if (user != null && !user.isEmpty()) {
@@ -636,7 +637,7 @@
       @Override
       public void run() {
         try (BufferedReader br =
-            new BufferedReader(new InputStreamReader(in, "ISO-8859-1"))) {
+            new BufferedReader(new InputStreamReader(in, ISO_8859_1.name()))) {
           String line;
           while ((line = br.readLine()) != null) {
             log.error("CGI: " + line);
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 0ac2c4f..d5259bd 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
@@ -17,6 +17,7 @@
 import static com.google.gerrit.common.FileUtil.lastModified;
 import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING;
 import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Optional;
@@ -468,13 +469,13 @@
     m.appendTail(sb);
 
     byte[] html = new MarkdownFormatter()
-      .markdownToDocHtml(sb.toString(), "UTF-8");
+      .markdownToDocHtml(sb.toString(), UTF_8.name());
     resourceCache.put(cacheKey, new SmallResource(html)
         .setContentType("text/html")
-        .setCharacterEncoding("UTF-8")
+        .setCharacterEncoding(UTF_8.name())
         .setLastModified(lastModifiedTime));
     res.setContentType("text/html");
-    res.setCharacterEncoding("UTF-8");
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(html.length);
     res.setDateHeader("Last-Modified", lastModifiedTime);
     res.getOutputStream().write(html);
@@ -526,7 +527,7 @@
       charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     }
     if (charEnc == null) {
-      charEnc = "UTF-8";
+      charEnc = UTF_8.name();
     }
     return new MarkdownFormatter().extractTitleFromMarkdown(
           readWholeEntry(scanner, entry),
@@ -553,7 +554,7 @@
     }
 
     String txtmd = RawParseUtils.decode(
-        Charset.forName(encoding != null ? encoding : "UTF-8"),
+        Charset.forName(encoding != null ? encoding : UTF_8.name()),
         rawmd);
     long time = entry.getTime();
     if (0 < time) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index 3cbb68b..ce696a1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -15,48 +15,23 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.common.base.Optional;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import eu.medsea.mimeutil.MimeType;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.NB;
-
-import java.io.FilterOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.security.MessageDigest;
-import java.security.SecureRandom;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -74,26 +49,17 @@
 @SuppressWarnings("serial")
 @Singleton
 public class CatServlet extends HttpServlet {
-  private static final MimeType ZIP = new MimeType("application/zip");
   private final Provider<ReviewDb> requestDb;
-  private final GitRepositoryManager repoManager;
-  private final SecureRandom rng;
-  private final FileTypeRegistry registry;
   private final Provider<CurrentUser> userProvider;
   private final ChangeControl.GenericFactory changeControl;
   private final ChangeEditUtil changeEditUtil;
 
   @Inject
-  CatServlet(GitRepositoryManager grm,
-      Provider<ReviewDb> sf,
-      FileTypeRegistry ftr,
+  CatServlet(Provider<ReviewDb> sf,
       ChangeControl.GenericFactory ccf,
       Provider<CurrentUser> usrprv,
       ChangeEditUtil ceu) {
     requestDb = sf;
-    repoManager = grm;
-    rng = new SecureRandom();
-    registry = ftr;
     changeControl = ccf;
     userProvider = usrprv;
     changeEditUtil = ceu;
@@ -130,7 +96,6 @@
 
       if (c < 0) {
         side = 0;
-
       } else {
         try {
           side = Integer.parseInt(keyStr.substring(c + 1));
@@ -150,15 +115,11 @@
     }
 
     final Change.Id changeId = patchKey.getParentKey().getParentKey();
-    final Project project;
-    final String revision;
+    String revision;
     try {
       final ReviewDb db = requestDb.get();
       final ChangeControl control = changeControl.validateFor(changeId,
           userProvider.get());
-
-      project = control.getProject();
-
       if (patchKey.getParentKey().get() == 0) {
         // change edit
         try {
@@ -190,184 +151,9 @@
       return;
     }
 
-    ObjectLoader blobLoader;
-    RevCommit fromCommit;
-    String suffix;
     String path = patchKey.getFileName();
-    try (Repository repo = repoManager.openRepository(project.getNameKey())) {
-      try (ObjectReader reader = repo.newObjectReader();
-          RevWalk rw = new RevWalk(reader)) {
-        RevCommit c;
-
-        c = rw.parseCommit(ObjectId.fromString(revision));
-        if (side == 0) {
-          fromCommit = c;
-          suffix = "new";
-
-        } else if (1 <= side && side - 1 < c.getParentCount()) {
-          fromCommit = rw.parseCommit(c.getParent(side - 1));
-          if (c.getParentCount() == 1) {
-            suffix = "old";
-          } else {
-            suffix = "old" + side;
-          }
-
-        } else {
-          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-          return;
-        }
-
-        try (TreeWalk tw = TreeWalk.forPath(reader, path, fromCommit.getTree())) {
-          if (tw == null) {
-            rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-            return;
-          }
-
-          if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
-            blobLoader = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
-
-          } else {
-            rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-            return;
-          }
-        }
-      }
-    } catch (RepositoryNotFoundException e) {
-      getServletContext().log("Cannot open repository", e);
-      rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    } catch (IOException | RuntimeException e) {
-      getServletContext().log("Cannot read repository", e);
-      rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-
-    final byte[] raw =
-        blobLoader.isLarge() ? null : blobLoader.getCachedBytes();
-    final long when = fromCommit.getCommitTime() * 1000L;
-
-    rsp.setDateHeader("Last-Modified", when);
-    CacheHeaders.setNotCacheable(rsp);
-
-    try (OutputStream out = openOutputStream(
-        req, rsp, blobLoader, fromCommit, when, path, suffix, raw)) {
-      if (raw != null) {
-        out.write(raw);
-      } else {
-        blobLoader.copyTo(out);
-      }
-    }
+    String restUrl = String.format("%s/changes/%d/revisions/%s/files/%s/download?parent=%d",
+        req.getContextPath(), changeId.get(), revision, Url.encode(path), side);
+    rsp.sendRedirect(restUrl);
   }
-
-  private OutputStream openOutputStream(HttpServletRequest req,
-      HttpServletResponse rsp, ObjectLoader blobLoader,
-      RevCommit fromCommit, long when, String path, String suffix, byte[] raw)
-      throws IOException {
-    MimeType contentType = registry.getMimeType(path, raw);
-    if (!registry.isSafeInline(contentType)) {
-      // The content may not be safe to transmit inline, as a browser might
-      // interpret it as HTML or JavaScript hosted by this site. Such code
-      // might then run in the site's security domain, and may be able to use
-      // the user's cookies to perform unauthorized actions.
-      //
-      // Usually, wrapping the content into a ZIP file forces the browser to
-      // save the content to the local system instead.
-      //
-
-      rsp.setContentType(ZIP.toString());
-      rsp.setHeader("Content-Disposition", "attachment; filename=\""
-          + safeFileName(path, suffix) + ".zip" + "\"");
-
-      final ZipOutputStream zo = new ZipOutputStream(rsp.getOutputStream());
-
-      ZipEntry e = new ZipEntry(safeFileName(path, rand(req, suffix)));
-      e.setComment(fromCommit.name() + ":" + path);
-      e.setSize(blobLoader.getSize());
-      e.setTime(when);
-      zo.putNextEntry(e);
-      return new FilterOutputStream(zo) {
-        @Override
-        public void close() throws IOException {
-          try {
-            zo.closeEntry();
-          } finally {
-            super.close();
-          }
-        }
-      };
-
-    } else {
-      rsp.setContentType(contentType.toString());
-      rsp.setHeader("Content-Length", "" + blobLoader.getSize());
-
-      return rsp.getOutputStream();
-    }
-  }
-
-  private static String safeFileName(String fileName, final String suffix) {
-    // Convert a file path (e.g. "src/Init.c") to a safe file name with
-    // no meta-characters that might be unsafe on any given platform.
-    //
-    final int slash = fileName.lastIndexOf('/');
-    if (slash >= 0) {
-      fileName = fileName.substring(slash + 1);
-    }
-
-    final StringBuilder r = new StringBuilder(fileName.length());
-    for (int i = 0; i < fileName.length(); i++) {
-      final char c = fileName.charAt(i);
-      if (c == '_' || c == '-' || c == '.' || c == '@') {
-        r.append(c);
-
-      } else if ('0' <= c && c <= '9') {
-        r.append(c);
-
-      } else if ('A' <= c && c <= 'Z') {
-        r.append(c);
-
-      } else if ('a' <= c && c <= 'z') {
-        r.append(c);
-
-      } else if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
-        r.append('-');
-
-      } else {
-        r.append('_');
-      }
-    }
-    fileName = r.toString();
-
-    final int ext = fileName.lastIndexOf('.');
-    if (ext <= 0) {
-      return fileName + "_" + suffix;
-
-    } else {
-      return fileName.substring(0, ext) + "_" + suffix
-          + fileName.substring(ext);
-    }
-  }
-
-  private String rand(final HttpServletRequest req, final String suffix)
-      throws UnsupportedEncodingException {
-    // Produce a random suffix that is difficult (or nearly impossible)
-    // for an attacker to guess in advance. This reduces the risk that
-    // an attacker could upload a *.class file and have us send a ZIP
-    // that can be invoked through an applet tag in the victim's browser.
-    //
-    final MessageDigest md = Constants.newMessageDigest();
-    final byte[] buf = new byte[8];
-
-    NB.encodeInt32(buf, 0, req.getRemotePort());
-    md.update(req.getRemoteAddr().getBytes("UTF-8"));
-    md.update(buf, 0, 4);
-
-    NB.encodeInt64(buf, 0, TimeUtil.nowMs());
-    md.update(buf, 0, 8);
-
-    rng.nextBytes(buf);
-    md.update(buf, 0, 8);
-
-    return suffix + "-" + ObjectId.fromRaw(md.digest()).name();
-  }
-
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index e499109..c1a113f 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.gerrit.common.FileUtil.lastModified;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
@@ -30,7 +31,6 @@
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -187,7 +187,7 @@
       w.write(";");
 
       w.write(HPD_ID + ".accountDiffPref=");
-      json(((IdentifiedUser) user).getAccountDiffPreference(), w);
+      json(user.asIdentifiedUser().getAccountDiffPreference(), w);
       w.write(";");
 
       w.write(HPD_ID + ".theme=");
@@ -201,7 +201,7 @@
     plugins(w);
     messages(w);
 
-    final byte[] hpd = w.toString().getBytes("UTF-8");
+    final byte[] hpd = w.toString().getBytes(UTF_8);
     final byte[] raw = Bytes.concat(page.part1, hpd, page.part2);
     final byte[] tosend;
     if (RPCServletUtils.acceptsGzipEncoding(req)) {
@@ -353,8 +353,8 @@
         if (p < 0) {
           throw new IOException("No tag in transformed host page HTML");
         }
-        part1 = raw.substring(0, p).getBytes("UTF-8");
-        part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes("UTF-8");
+        part1 = raw.substring(0, p).getBytes(UTF_8);
+        part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes(UTF_8);
       }
     }
 
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 5f526dd..f570cb6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.raw;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
@@ -88,7 +90,7 @@
     }
 
     CacheHeaders.setNotCacheable(rsp);
-    rsp.setCharacterEncoding("UTF-8");
+    rsp.setCharacterEncoding(UTF_8.name());
     rsp.setContentType("text/plain");
     try (PrintWriter w = rsp.getWriter()) {
       w.write(out);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
index 179b268..5c6480a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.httpd.HtmlDomUtil.compress;
 import static com.google.gerrit.httpd.HtmlDomUtil.newDocument;
 import static com.google.gerrit.httpd.HtmlDomUtil.toUTF8;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static org.eclipse.jgit.util.HttpSupport.HDR_CACHE_CONTROL;
 import static org.eclipse.jgit.util.HttpSupport.HDR_EXPIRES;
@@ -143,7 +144,7 @@
     rsp.setHeader(HDR_PRAGMA, "no-cache");
     rsp.setHeader(HDR_CACHE_CONTROL, "no-cache, must-revalidate");
     rsp.setContentType("text/html");
-    rsp.setCharacterEncoding("UTF-8");
+    rsp.setCharacterEncoding(UTF_8.name());
     rsp.setContentLength(tosend.length);
     try (OutputStream out = rsp.getOutputStream()) {
       out.write(tosend);
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 2f2575d..7347cd9 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
@@ -669,7 +669,7 @@
     w.flush();
     replyBinaryResult(req, res, asBinaryResult(buf)
       .setContentType(JSON_TYPE)
-      .setCharacterEncoding(UTF_8.name()));
+      .setCharacterEncoding(UTF_8));
   }
 
   private static Gson newGson(Multimap<String, String> config,
@@ -791,7 +791,7 @@
     res.setHeader("X-FYI-Content-Type", src.getContentType());
     return asBinaryResult(buf)
       .setContentType(JSON_TYPE)
-      .setCharacterEncoding(UTF_8.name());
+      .setCharacterEncoding(UTF_8);
   }
 
   private static BinaryResult stackBase64(HttpServletResponse res,
@@ -819,7 +819,7 @@
     }
     res.setHeader("X-FYI-Content-Encoding", "base64");
     res.setHeader("X-FYI-Content-Type", src.getContentType());
-    return b64.setContentType("text/plain").setCharacterEncoding("ISO-8859-1");
+    return b64.setContentType("text/plain").setCharacterEncoding(ISO_8859_1);
   }
 
   private static BinaryResult stackGzip(HttpServletResponse res,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index 73b546b..547bf45 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -42,13 +41,10 @@
 
   protected Account.Id getAccountId() {
     CurrentUser u = currentUser.get();
-    if (u.isIdentifiedUser()) {
-      return ((IdentifiedUser) u).getAccountId();
-    }
-    return null;
+    return u.isIdentifiedUser() ? u.getAccountId() : null;
   }
 
-  protected CurrentUser getCurrentUser() {
+  protected CurrentUser getUser() {
     return currentUser.get();
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index 01f2df3..3b064e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -134,7 +134,7 @@
       Audit note = method.getAnnotation(Audit.class);
       if (note != null) {
         final String sid = call.getWebSession().getSessionId();
-        final CurrentUser username = call.getWebSession().getCurrentUser();
+        final CurrentUser username = call.getWebSession().getUser();
         final Multimap<String, ?> args =
             extractParams(note, call);
         final String what = extractWhat(note, call);
@@ -166,14 +166,18 @@
   }
 
   private String extractWhat(final Audit note, final GerritCall call) {
-    String methodClass = call.getMethodClass().getName();
-    methodClass = methodClass.substring(methodClass.lastIndexOf(".")+1);
+    Class<?> methodClass = call.getMethodClass();
+    String methodClassName = methodClass != null
+        ? methodClass.getName()
+        : "<UNKNOWN_CLASS>";
+    methodClassName =
+        methodClassName.substring(methodClassName.lastIndexOf(".") + 1);
     String what = note.action();
     if (what.length() == 0) {
       what = call.getMethod().getName();
     }
 
-    return methodClass + "." + what;
+    return methodClassName + "." + what;
   }
 
   static class GerritCall extends ActiveCall {
@@ -275,7 +279,7 @@
       } else if (session.isSignedIn() && session.isValidXGerritAuth(keyIn)) {
         // The session must exist, and must be using this token.
         //
-        session.getCurrentUser().setAccessPath(AccessPath.JSON_RPC);
+        session.getUser().setAccessPath(AccessPath.JSON_RPC);
         return true;
       }
       return false;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 34066f1..968029c 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
@@ -141,7 +141,7 @@
 
         AccountProjectWatch watch =
             new AccountProjectWatch(new AccountProjectWatch.Key(
-                ((IdentifiedUser) ctl.getCurrentUser()).getAccountId(),
+                ctl.getUser().getAccountId(),
                 nameKey, filter));
         try {
           db.accountProjectWatches().insert(Collections.singleton(watch));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 3237873..2bf5fe4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -14,16 +14,11 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.common.base.Function;
 import com.google.common.base.Optional;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.common.data.UiCommandDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -36,14 +31,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
-import com.google.gerrit.server.change.ChangesCollection;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -57,7 +47,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
@@ -86,8 +75,6 @@
   private final PatchListCache patchListCache;
   private final Provider<CurrentUser> userProvider;
   private final ChangeControl.GenericFactory changeControlFactory;
-  private final ChangesCollection changes;
-  private final Revisions revisions;
   private final PatchLineCommentsUtil plcUtil;
   private final ChangeEditUtil editUtil;
 
@@ -107,8 +94,6 @@
       final PatchListCache patchListCache,
       final Provider<CurrentUser> userProvider,
       final ChangeControl.GenericFactory changeControlFactory,
-      final ChangesCollection changes,
-      final Revisions revisions,
       final PatchLineCommentsUtil plcUtil,
       ChangeEditUtil editUtil,
       @Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
@@ -119,8 +104,6 @@
     this.patchListCache = patchListCache;
     this.userProvider = userProvider;
     this.changeControlFactory = changeControlFactory;
-    this.changes = changes;
-    this.revisions = revisions;
     this.plcUtil = plcUtil;
     this.editUtil = editUtil;
 
@@ -193,13 +176,13 @@
     detail.setInfo(infoFactory.get(db, patchSet.getId()));
     detail.setPatches(patches);
 
-    final CurrentUser user = control.getCurrentUser();
+    final CurrentUser user = control.getUser();
     if (user.isIdentifiedUser() && edit == null) {
       // If we are signed in, compute the number of draft comments by the
       // current user on each of these patch files. This way they can more
       // quickly locate where they have pending drafts, and review them.
       //
-      final Account.Id me = ((IdentifiedUser) user).getAccountId();
+      final Account.Id me = user.getAccountId();
       for (PatchLineComment c
           : plcUtil.draftByPatchSetAuthor(db, psIdNew, me, notes)) {
         final Patch p = byKey.get(c.getKey().getParentKey());
@@ -216,23 +199,6 @@
       }
     }
 
-    detail.setCommands(Lists.newArrayList(Iterables.transform(
-        UiActions.sorted(UiActions.plugins(UiActions.from(
-          revisions,
-          new RevisionResource(changes.parse(control), patchSet),
-          Providers.of(user)))),
-        new Function<UiAction.Description, UiCommandDetail>() {
-          @Override
-          public UiCommandDetail apply(UiAction.Description in) {
-            UiCommandDetail r = new UiCommandDetail();
-            r.method = in.getMethod();
-            r.id = in.getId();
-            r.label = in.getLabel();
-            r.title = in.getTitle();
-            r.enabled = in.isEnabled();
-            return r;
-          }
-        })));
     return detail;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index 9257271..7ff3782 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -60,7 +60,7 @@
       public PatchScript call() throws Exception {
         ChangeControl control = changeControlFactory.validateFor(
             patchKey.getParentKey().getParentKey(),
-            getCurrentUser());
+            getUser());
         return patchScriptFactoryFactory.create(
             control, patchKey.getFileName(), psa, psb, dp).call();
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index dff2cd0..5b3b064 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -20,7 +20,6 @@
 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;
@@ -37,8 +36,11 @@
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -49,7 +51,9 @@
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 import java.util.List;
@@ -70,6 +74,7 @@
   private final ProjectCache projectCache;
   private final ChangesCollection changes;
   private final ChangeInserter.Factory changeInserterFactory;
+  private final BatchUpdate.Factory updateFactory;
 
   @Inject
   ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
@@ -81,6 +86,7 @@
       AllProjectsNameProvider allProjects,
       ChangesCollection changes,
       ChangeInserter.Factory changeInserterFactory,
+      BatchUpdate.Factory updateFactory,
       Provider<SetParent> setParent,
 
       @Assisted("projectName") Project.NameKey projectName,
@@ -97,6 +103,7 @@
     this.projectCache = projectCache;
     this.changes = changes;
     this.changeInserterFactory = changeInserterFactory;
+    this.updateFactory = updateFactory;
   }
 
   @Override
@@ -120,9 +127,21 @@
             config.getProject().getNameKey(),
             RefNames.REFS_CONFIG),
         TimeUtil.nowTs());
-    ChangeInserter ins =
-        changeInserterFactory.create(ctl, change, commit);
-    ins.insert();
+    try (RevWalk rw = new RevWalk(md.getRepository());
+        ObjectInserter objInserter = md.getRepository().newObjectInserter();
+        BatchUpdate bu = updateFactory.create(
+          db, change.getProject(), ctl.getUser(),
+          change.getCreatedOn())) {
+      bu.setRepository(md.getRepository(), rw, objInserter);
+      bu.insertChange(
+          changeInserterFactory.create(
+                ctl.controlForRef(change.getDest().get()), change, commit)
+              .setValidatePolicy(CommitValidators.Policy.NONE)
+              .setUpdateRef(false)); // Created by commitToNewRef.
+      bu.execute();
+    } catch (UpdateException | RestApiException e) {
+      throw new IOException(e);
+    }
 
     ChangeResource rsrc;
     try {
@@ -152,7 +171,7 @@
       AddReviewerInput input = new AddReviewerInput();
       input.reviewer = projectOwners;
       reviewersProvider.get().apply(rsrc, input);
-    } catch (IOException | OrmException | RestApiException | EmailException e) {
+    } catch (IOException | OrmException | RestApiException e) {
       // one of the owner groups is not visible to the user and this it why it
       // can't be added as reviewer
     }
@@ -168,7 +187,7 @@
         AddReviewerInput input = new AddReviewerInput();
         input.reviewer = r.getGroup().getUUID().get();
         reviewersProvider.get().apply(rsrc, input);
-      } catch (IOException | OrmException | RestApiException | EmailException e) {
+      } catch (IOException | OrmException | RestApiException e) {
         // ignore
       }
     }
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 60404a3..89c8ec6 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
@@ -29,6 +29,8 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.CodeSource;
 import java.util.ArrayList;
 import java.util.Enumeration;
@@ -64,14 +66,15 @@
       System.err.println("usage: java -jar " + jar + " command [ARG ...]");
       System.err.println();
       System.err.println("The most commonly used commands are:");
-      System.err.println("  init           Initialize a Gerrit installation");
-      System.err.println("  reindex        Rebuild the secondary index");
-      System.err.println("  daemon         Run the Gerrit network daemons");
-      System.err.println("  gsql           Run the interactive query console");
-      System.err.println("  version        Display the build version number");
+      System.err.println("  init            Initialize a Gerrit installation");
+      System.err.println("  rebuild-notedb  Rebuild the review notes database");
+      System.err.println("  reindex         Rebuild the secondary index");
+      System.err.println("  daemon          Run the Gerrit network daemons");
+      System.err.println("  gsql            Run the interactive query console");
+      System.err.println("  version         Display the build version number");
       System.err.println();
-      System.err.println("  ls             List files available for cat");
-      System.err.println("  cat FILE       Display a file from the archive");
+      System.err.println("  ls              List files available for cat");
+      System.err.println("  cat FILE        Display a file from the archive");
       System.err.println();
       return 1;
     }
@@ -540,7 +543,7 @@
    *
    * @throws FileNotFoundException if the directory cannot be found.
    */
-  public static File getDeveloperBuckOut() throws FileNotFoundException {
+  public static Path getDeveloperBuckOut() throws FileNotFoundException {
     // Find ourselves in the CLASSPATH, we should be a loose class file.
     Class<GerritLauncher> self = GerritLauncher.class;
     URL u = self.getResource(self.getSimpleName() + ".class");
@@ -551,39 +554,43 @@
     }
 
     // Pop up to the top level classes folder that contains us.
-    File dir = new File(u.getPath());
+    Path dir = Paths.get(u.getPath());
     String myName = self.getName();
     for (;;) {
       int dot = myName.lastIndexOf('.');
       if (dot < 0) {
-        dir = dir.getParentFile();
+        dir = dir.getParent();
         break;
       }
       myName = myName.substring(0, dot);
-      dir = dir.getParentFile();
+      dir = dir.getParent();
     }
 
     dir = popdir(u, dir, "classes");
     dir = popdir(u, dir, "eclipse");
-    if ("buck-out".equals(dir.getName())) {
+    if (last(dir).equals("buck-out")) {
       return dir;
     }
     throw new FileNotFoundException("Cannot find buck-out from " + u);
   }
 
-  private static File popdir(URL u, File dir, String name)
+  private static String last(Path dir) {
+    return dir.getName(dir.getNameCount() - 1).toString();
+  }
+
+  private static Path popdir(URL u, Path dir, String name)
       throws FileNotFoundException {
-    if (dir.getName().equals(name)) {
-      return dir.getParentFile();
+    if (last(dir).equals(name)) {
+      return dir.getParent();
     }
     throw new FileNotFoundException("Cannot find buck-out from " + u);
   }
 
   private static ClassLoader useDevClasspath()
       throws MalformedURLException, FileNotFoundException {
-    File out = getDeveloperBuckOut();
+    Path out = getDeveloperBuckOut();
     List<URL> dirs = new ArrayList<>();
-    dirs.add(new File(new File(out, "eclipse"), "classes").toURI().toURL());
+    dirs.add(out.resolve("eclipse").resolve("classes").toUri().toURL());
     ClassLoader cl = GerritLauncher.class.getClassLoader();
     for (URL u : ((URLClassLoader) cl).getURLs()) {
       if (includeJar(u)) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
index 27ded17..4e47bca 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
@@ -25,13 +25,28 @@
 import java.io.IOException;
 
 /** Writer that optionally flushes/commits after every write. */
-class AutoCommitWriter extends IndexWriter {
+public class AutoCommitWriter extends IndexWriter {
   private boolean autoCommit;
 
+  AutoCommitWriter(Directory dir, IndexWriterConfig config)
+      throws IOException {
+    this(dir, config, false);
+  }
+
   AutoCommitWriter(Directory dir, IndexWriterConfig config, boolean autoCommit)
       throws IOException {
     super(dir, config);
-    this.autoCommit = autoCommit;
+    setAutoCommit(autoCommit);
+  }
+
+  /**
+   * This method will override Gerrit configuration index.name.commitWithin
+   * until next Gerrit restart (or reconfiguration through this method).
+   *
+   * @param enable auto commit
+   */
+  public void setAutoCommit(boolean enable) {
+    this.autoCommit = enable;
   }
 
   @Override
@@ -99,7 +114,7 @@
     }
   }
 
-  private void autoFlush() throws IOException {
+  public void autoFlush() throws IOException {
     if (autoCommit) {
       manualFlush();
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 55ed22d..99003ee 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
@@ -386,6 +386,14 @@
     }
   }
 
+  public SubIndex getOpenChangesIndex() {
+    return openIndex;
+  }
+
+  public SubIndex getClosedChangesIndex() {
+    return closedIndex;
+  }
+
   private class QuerySource implements ChangeDataSource {
     private final List<SubIndex> indexes;
     private final Query query;
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 7fd98aa..939c1fb 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
@@ -82,11 +82,11 @@
   private Query or(Predicate<ChangeData> p)
       throws QueryParseException {
     try {
-      BooleanQuery q = new BooleanQuery();
+      BooleanQuery.Builder q = new BooleanQuery.Builder();
       for (int i = 0; i < p.getChildCount(); i++) {
         q.add(toQuery(p.getChild(i)), SHOULD);
       }
-      return q;
+      return q.build();
     } catch (BooleanQuery.TooManyClauses e) {
       throw new QueryParseException("cannot create query for index: " + p, e);
     }
@@ -95,7 +95,7 @@
   private Query and(Predicate<ChangeData> p)
       throws QueryParseException {
     try {
-      BooleanQuery b = new BooleanQuery();
+      BooleanQuery.Builder b = new BooleanQuery.Builder();
       List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
       for (int i = 0; i < p.getChildCount(); i++) {
         Predicate<ChangeData> c = p.getChild(i);
@@ -113,7 +113,7 @@
       for (Query q : not) {
         b.add(q, MUST_NOT);
       }
-      return b;
+      return b.build();
     } catch (BooleanQuery.TooManyClauses e) {
       throw new QueryParseException("cannot create query for index: " + p, e);
     }
@@ -127,10 +127,10 @@
     }
 
     // Lucene does not support negation, start with all and subtract.
-    BooleanQuery q = new BooleanQuery();
-    q.add(new MatchAllDocsQuery(), MUST);
-    q.add(toQuery(n), MUST_NOT);
-    return q;
+    return new BooleanQuery.Builder()
+      .add(new MatchAllDocsQuery(), MUST)
+      .add(toQuery(n), MUST_NOT)
+      .build();
   }
 
   private Query fieldQuery(IndexPredicate<ChangeData> p)
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 5778008..bb69533bf 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
@@ -47,7 +47,7 @@
 import java.util.concurrent.TimeoutException;
 
 /** Piece of the change index that is implemented as a separate Lucene index. */
-class SubIndex {
+public class SubIndex {
   private static final Logger log = LoggerFactory.getLogger(SubIndex.class);
 
   private final Directory dir;
@@ -70,13 +70,13 @@
     long commitPeriod = writerConfig.getCommitWithinMs();
 
     if (commitPeriod < 0) {
-      delegateWriter = new IndexWriter(dir, writerConfig.getLuceneConfig());
+      delegateWriter = new AutoCommitWriter(dir, writerConfig.getLuceneConfig());
     } else if (commitPeriod == 0) {
       delegateWriter =
           new AutoCommitWriter(dir, writerConfig.getLuceneConfig(), true);
     } else {
       final AutoCommitWriter autoCommitWriter =
-          new AutoCommitWriter(dir, writerConfig.getLuceneConfig(), false);
+          new AutoCommitWriter(dir, writerConfig.getLuceneConfig());
       delegateWriter = autoCommitWriter;
 
       new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
@@ -191,6 +191,10 @@
     writer.deleteAll();
   }
 
+  public TrackingIndexWriter getWriter() {
+    return writer;
+  }
+
   IndexSearcher acquire() throws IOException {
     return searcherManager.acquire();
   }
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
index 2d73634..333af15 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.oauth;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
@@ -32,7 +34,6 @@
 import org.w3c.dom.Element;
 
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
@@ -183,7 +184,7 @@
     byte[] bin = HtmlDomUtil.toUTF8(doc);
     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     res.setContentType("text/html");
-    res.setCharacterEncoding(StandardCharsets.UTF_8.name());
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(bin.length);
     try (ServletOutputStream out = res.getOutputStream()) {
       out.write(bin);
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index bd7558b..8b05c72 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
@@ -164,11 +166,14 @@
       mode = SignInMode.SIGN_IN;
     }
 
+    log.debug("mode \"{}\"", mode);
     OAuthServiceProvider oauthProvider = lookupOAuthServiceProvider(id);
 
     if (oauthProvider == null) {
+      log.debug("OpenId provider \"{}\"", id);
       discover(req, res, link, id, remember, token, mode);
     } else {
+      log.debug("OAuth provider \"{}\"", id);
       OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get();
       if (!currentUserProvider.get().isIdentifiedUser()
           && oauthSession.isLoggedIn()) {
@@ -317,7 +322,7 @@
     byte[] bin = HtmlDomUtil.toUTF8(doc);
     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     res.setContentType("text/html");
-    res.setCharacterEncoding("UTF-8");
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(bin.length);
     try (ServletOutputStream out = res.getOutputStream()) {
       out.write(bin);
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index 6d129bf..8d5d4b9 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -127,10 +127,12 @@
       Account.Id actualId = accountManager.lookup(user.getExternalId());
       // Use case 1: claimed identity was provided during handshake phase
       if (!Strings.isNullOrEmpty(claimedIdentifier)) {
+        log.debug("Claimed identity is set");
         Account.Id claimedId = accountManager.lookup(claimedIdentifier);
         if (claimedId != null && actualId != null) {
           if (claimedId.equals(actualId)) {
             // Both link to the same account, that's what we expected.
+            log.debug("Both link to the same account. All is fine.");
           } else {
             // This is (for now) a fatal error. There are two records
             // for what might be the same user.
@@ -144,7 +146,7 @@
           }
         } else if (claimedId != null && actualId == null) {
           // Claimed account already exists: link to it.
-          //
+          log.debug("Claimed account already exists: link to it.");
           try {
             accountManager.link(claimedId, areq);
           } catch (OrmException e) {
@@ -157,11 +159,14 @@
         }
       } else if (linkMode) {
         // Use case 2: link mode activated from the UI
+        Account.Id accountId = identifiedUser.get().getAccountId();
         try {
-          accountManager.link(identifiedUser.get().getAccountId(), areq);
+          log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(),
+              accountId);
+          accountManager.link(accountId, areq);
         } catch (OrmException e) {
           log.error("Cannot link: " + user.getExternalId()
-              + " to user identity: " + identifiedUser.get().getAccountId());
+              + " to user identity: " + accountId);
           rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
           return;
         } finally {
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
index d6ada97..b48d3ed 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -29,7 +31,6 @@
 @Singleton
 class XrdsServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  private static final String ENC = "UTF-8";
   static final String LOCATION = "OpenID.XRDS";
 
   private final Provider<String> url;
@@ -43,7 +44,8 @@
   protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
     final StringBuilder r = new StringBuilder();
-    r.append("<?xml version=\"1.0\" encoding=\"").append(ENC).append("\"?>");
+    r.append("<?xml version=\"1.0\" encoding=\"")
+     .append(UTF_8.name()).append("\"?>");
     r.append("<xrds:XRDS");
     r.append(" xmlns:xrds=\"xri://$xrds\"");
     r.append(" xmlns:openid=\"http://openid.net/xmlns/1.0\"");
@@ -58,10 +60,10 @@
     r.append("</xrds:XRDS>");
     r.append("\n");
 
-    final byte[] raw = r.toString().getBytes(ENC);
+    final byte[] raw = r.toString().getBytes(UTF_8);
     rsp.setContentLength(raw.length);
     rsp.setContentType("application/xrds+xml");
-    rsp.setCharacterEncoding(ENC);
+    rsp.setCharacterEncoding(UTF_8.name());
 
     try (ServletOutputStream out = rsp.getOutputStream()) {
       out.write(raw);
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index eb4ebab..c5a9d0c 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -14,6 +14,8 @@
 
 package org.apache.commons.net.smtp;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
 
 import org.apache.commons.codec.binary.Base64;
@@ -35,8 +37,6 @@
 import javax.net.ssl.SSLSocketFactory;
 
 public class AuthSMTPClient extends SMTPClient {
-  private static final String UTF_8 = "UTF-8";
-
   private String authTypes;
 
   public AuthSMTPClient(final String charset) {
@@ -65,11 +65,9 @@
     _input_ = _socket_.getInputStream();
     _output_ = _socket_.getOutputStream();
     _reader =
-        new BufferedReader(new InputStreamReader(_input_,
-                      UTF_8));
+        new BufferedReader(new InputStreamReader(_input_, UTF_8));
     _writer =
-        new BufferedWriter(new OutputStreamWriter(_output_,
-                      UTF_8));
+        new BufferedWriter(new OutputStreamWriter(_output_, UTF_8));
     return true;
   }
 
@@ -190,7 +188,7 @@
     return SMTPReply.isPositiveCompletion(sendCommand("AUTH", cmd));
   }
 
-  private static String encodeBase64(final byte[] data) throws UnsupportedEncodingException {
+  private static String encodeBase64(final byte[] data) {
     return new String(Base64.encodeBase64(data), UTF_8);
   }
 }
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 80c37f7..c57ec52 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -45,6 +45,7 @@
     '//gerrit-common:annotations',
     '//gerrit-lucene:lucene',
     '//lib:args4j',
+    '//lib:derby',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
     '//lib:h2',
@@ -53,21 +54,24 @@
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
+    '//gerrit-acceptance-framework/...',
     '//gerrit-acceptance-tests/...',
     '//gerrit-war:',
   ],
 )
 
+REST_UTIL_DEPS = [
+  '//gerrit-cache-h2:cache-h2',
+  '//gerrit-util-cli:cli',
+  '//lib:args4j',
+  '//lib:gwtorm',
+  '//lib/commons:dbcp',
+]
+
 java_library(
   name = 'util',
-  srcs = glob([SRCS + 'util/*.java']),
-  deps = DEPS + [
-    '//gerrit-cache-h2:cache-h2',
-    '//gerrit-util-cli:cli',
-    '//lib:args4j',
-    '//lib:gwtorm',
-    '//lib/commons:dbcp',
-  ],
+  deps = DEPS + REST_UTIL_DEPS,
+  exported_deps = [':util-nodep'],
   visibility = [
     '//gerrit-acceptance-tests/...',
     '//gerrit-gwtdebug:gwtdebug',
@@ -76,6 +80,15 @@
 )
 
 java_library(
+  name = 'util-nodep',
+  srcs = glob([SRCS + 'util/*.java']),
+  provided_deps = DEPS + REST_UTIL_DEPS,
+  visibility = [
+    '//gerrit-acceptance-framework/...',
+  ],
+)
+
+java_library(
   name = 'http',
   srcs = glob([SRCS + 'http/**/*.java']),
   deps = DEPS + [
@@ -90,30 +103,32 @@
   visibility = ['//gerrit-war:'],
 )
 
+REST_PGM_DEPS = [
+  ':http',
+  ':init',
+  ':init-api',
+  ':util',
+  '//gerrit-cache-h2:cache-h2',
+  '//gerrit-gpg:gpg',
+  '//gerrit-lucene:lucene',
+  '//gerrit-oauth:oauth',
+  '//gerrit-openid:openid',
+  '//lib:args4j',
+  '//lib:gwtorm',
+  '//lib:protobuf',
+  '//lib:servlet-api-3_1',
+  '//lib/auto:auto-value',
+  '//lib/prolog:cafeteria',
+  '//lib/prolog:compiler',
+  '//lib/prolog:runtime',
+]
+
 java_library(
   name = 'pgm',
-  srcs = glob([SRCS + '*.java', SRCS + 'rules/*.java']),
   resources = glob([RSRCS + '*']),
-  deps = DEPS + [
-    ':http',
-    ':init',
-    ':init-api',
-    ':util',
-    '//gerrit-cache-h2:cache-h2',
-    '//gerrit-gpg:gpg',
-    '//gerrit-lucene:lucene',
-    '//gerrit-oauth:oauth',
-    '//gerrit-openid:openid',
-    '//lib:args4j',
-    '//lib:gwtorm',
-    '//lib:protobuf',
-    '//lib:servlet-api-3_1',
-    '//lib/auto:auto-value',
-    '//lib/prolog:cafeteria',
-    '//lib/prolog:compiler',
-    '//lib/prolog:runtime',
+  deps = DEPS + REST_PGM_DEPS + [
+    ':daemon',
   ],
-  provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
     '//:',
     '//gerrit-acceptance-tests/...',
@@ -123,6 +138,21 @@
   ],
 )
 
+# no transitive deps, used for gerrit-acceptance-framework
+java_library(
+  name = 'daemon',
+  srcs = glob([SRCS + '*.java', SRCS + 'rules/*.java']),
+  resources = glob([RSRCS + '*']),
+  deps = ['//lib/auto:auto-value'],
+  provided_deps = DEPS + REST_PGM_DEPS + [
+    '//gerrit-launcher:launcher',
+  ],
+  visibility = [
+    '//gerrit-acceptance-framework/...',
+    '//gerrit-gwtdebug:gwtdebug',
+  ],
+)
+
 java_test(
   name = 'pgm_tests',
   srcs = glob(['src/test/java/**/*.java']),
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 da87a20..39a5cbd 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
@@ -80,6 +80,7 @@
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
+import com.google.gerrit.sshd.commands.IndexCommandsModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -155,6 +156,7 @@
   private Module emailModule;
 
   private Runnable serverStarted;
+  private IndexType indexType;
 
   public Daemon() {
   }
@@ -276,6 +278,7 @@
     cfgInjector = createCfgInjector();
     config = cfgInjector.getInstance(
         Key.get(Config.class, GerritServerConfig.class));
+    initIndexType();
     sysInjector = createSysInjector();
     sysInjector.getInstance(PluginGuiceEnvironment.class)
       .setDbCfgInjector(dbInjector, cfgInjector);
@@ -379,7 +382,6 @@
     if (slave) {
       return new DummyIndexModule();
     }
-    IndexType indexType = IndexModule.getIndexType(cfgInjector);
     switch (indexType) {
       case LUCENE:
         return luceneModule != null ? luceneModule : new LuceneIndexModule();
@@ -388,6 +390,16 @@
     }
   }
 
+  private void initIndexType() {
+    indexType = IndexModule.getIndexType(cfgInjector);
+    switch (indexType) {
+      case LUCENE:
+        break;
+      default:
+        throw new IllegalStateException("unsupported index.type = " + indexType);
+    }
+  }
+
   private void initSshd() {
     sshInjector = createSshInjector();
     sysInjector.getInstance(PluginGuiceEnvironment.class)
@@ -403,6 +415,9 @@
     }
     modules.add(new DefaultCommandModule(slave,
         sysInjector.getInstance(DownloadConfig.class)));
+    if (indexType == IndexType.LUCENE) {
+      modules.add(new IndexCommandsModule());
+    }
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
index a77cc8c..84741d4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.pgm.util.AbstractProgram;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.schema.java.JavaSchemaModel;
@@ -45,13 +47,13 @@
       JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
       try (OutputStream o = lock.getOutputStream();
           PrintWriter out = new PrintWriter(
-              new BufferedWriter(new OutputStreamWriter(o, "UTF-8")))) {
+              new BufferedWriter(new OutputStreamWriter(o, UTF_8)))) {
         String header;
         try (InputStream in = getClass().getResourceAsStream("ProtoGenHeader.txt")) {
           ByteBuffer buf = IO.readWholeStream(in, 1024);
           int ptr = buf.arrayOffset() + buf.position();
           int len = buf.remaining();
-          header = new String(buf.array(), ptr, len, "UTF-8");
+          header = new String(buf.array(), ptr, len, UTF_8);
         }
 
         String version = com.google.gerrit.common.Version.getVersion();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
index 8b90e54..dd5fe0c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
@@ -25,6 +25,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.common.FormatUtil;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleManager;
@@ -76,7 +77,8 @@
   private static final Logger log =
       LoggerFactory.getLogger(RebuildNotedb.class);
 
-  @Option(name = "--threads", usage = "Number of threads to use for indexing")
+  @Option(name = "--threads",
+      usage = "Number of threads to use for rebuilding NoteDb")
   private int threads = Runtime.getRuntime().availableProcessors();
 
   private Injector dbInjector;
@@ -123,10 +125,10 @@
               allUsersRepo.getRefDatabase().newBatchUpdate();
           List<ListenableFuture<?>> futures = Lists.newArrayList();
 
-          // Here, we truncate the project name to 50 characters to ensure that
+          // Here, we elide the project name to 50 characters to ensure that
           // the whole monitor line for a project fits on one line (<80 chars).
           final MultiProgressMonitor mpm = new MultiProgressMonitor(System.out,
-              truncateProjectName(project.get()));
+              FormatUtil.elide(project.get(), 50));
           final Task doneTask =
               mpm.beginSubTask("done", changesByProject.get(project).size());
           final Task failedTask =
@@ -166,17 +168,6 @@
     return ok.get() ? 0 : 1;
   }
 
-  private static String truncateProjectName(String projectName) {
-    int monitorStringMaxLength = 50;
-    String monitorString = (projectName.length() > monitorStringMaxLength)
-        ? projectName.substring(0, monitorStringMaxLength)
-        : projectName;
-    if (projectName.length() > monitorString.length()) {
-      monitorString = monitorString + "...";
-    }
-    return monitorString;
-  }
-
   private static void execute(BatchRefUpdate bru, Repository repo)
       throws IOException {
     try (RevWalk rw = new RevWalk(repo)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
index ba1aea3..1b663ae 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.pgm.http.jetty;
 
-import com.google.common.base.Charsets;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
 import com.google.common.base.Strings;
 import com.google.gwtexpui.server.CacheHeaders;
 
@@ -68,7 +69,7 @@
       msg = HttpStatus.getMessage(conn.getHttpChannel()
           .getResponse().getStatus());
     }
-    return msg.getBytes(Charsets.ISO_8859_1);
+    return msg.getBytes(ISO_8859_1);
   }
 
   private static void log(HttpServletRequest req) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 9f94dbf..25b351e 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
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.pgm.http.jetty;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.common.base.Charsets;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.escape.Escaper;
@@ -83,6 +83,7 @@
 import java.net.URISyntaxException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.Enumeration;
@@ -562,9 +563,9 @@
 
   private Resource useDeveloperBuild(ServletContextHandler app)
       throws IOException {
-    final File dir = GerritLauncher.getDeveloperBuckOut();
-    final File gen = new File(dir, "gen");
-    final File root = dir.getParentFile();
+    final Path dir = GerritLauncher.getDeveloperBuckOut();
+    final Path gen = dir.resolve("gen");
+    final Path root = dir.getParent();
     final File dstwar = makeWarTempDir();
     File ui = new File(dstwar, "gerrit_ui");
     File p = new File(ui, "permutations");
@@ -593,7 +594,7 @@
           // $ buck targets --show_output //gerrit-gwtui:ui_safari \
           //    | awk '{print $2}'
           String child = String.format("%s/__gwt_binary_%s__", pkg, target);
-          File zip = new File(new File(gen, child), target + ".zip");
+          File zip = gen.resolve(child).resolve(target + ".zip").toFile();
 
           synchronized (this) {
             try {
@@ -618,7 +619,7 @@
           throws IOException {
         res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
         res.setContentType("text/html");
-        res.setCharacterEncoding(Charsets.UTF_8.name());
+        res.setCharacterEncoding(UTF_8.name());
         CacheHeaders.setNotCacheable(res);
 
         Escaper html = HtmlEscapers.htmlEscaper();
@@ -643,13 +644,13 @@
     return Resource.newResource(dstwar.toURI());
   }
 
-  private static void build(File root, File gen, String target)
+  private static void build(Path root, Path gen, String target)
       throws IOException, BuildFailureException {
     log.info("buck build " + target);
     Properties properties = loadBuckProperties(gen);
     String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
     ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
-        .directory(root)
+        .directory(root.toFile())
         .redirectErrorStream(true);
     if (properties.containsKey("PATH")) {
       proc.environment().put("PATH", properties.getProperty("PATH"));
@@ -677,11 +678,11 @@
     log.info(String.format("UPDATED    %s in %.3fs", target, time / 1000.0));
   }
 
-  private static Properties loadBuckProperties(File gen)
+  private static Properties loadBuckProperties(Path gen)
       throws FileNotFoundException, IOException {
     Properties properties = new Properties();
     try (InputStream in = new FileInputStream(
-        new File(new File(gen, "tools"), "buck.properties"))) {
+        gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) {
       properties.load(in);
     }
     return properties;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index 7730fa5..0f75a09 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -20,7 +20,6 @@
 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
 
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
@@ -84,17 +83,17 @@
     }
   }
 
-  private final Provider<CurrentUser> userProvider;
+  private final Provider<CurrentUser> user;
   private final QueueProvider queue;
 
   private final ServletContext context;
   private final long maxWait;
 
   @Inject
-  ProjectQoSFilter(final Provider<CurrentUser> userProvider,
+  ProjectQoSFilter(final Provider<CurrentUser> user,
       QueueProvider queue, final ServletContext context,
       @GerritServerConfig final Config cfg) {
-    this.userProvider = userProvider;
+    this.user = user;
     this.queue = queue;
     this.context = context;
     this.maxWait = MINUTES.toMillis(getTimeUnit(cfg, "httpd", null, "maxwait", 5, MINUTES));
@@ -142,7 +141,7 @@
   }
 
   private WorkQueue.Executor getExecutor() {
-    return queue.getQueue(userProvider.get().getCapabilities().getQueueType());
+    return queue.getQueue(user.get().getCapabilities().getQueueType());
   }
 
   @Override
@@ -226,9 +225,9 @@
     private String generateName(HttpServletRequest req) {
       String userName = "";
 
-      CurrentUser who = userProvider.get();
+      CurrentUser who = user.get();
       if (who.isIdentifiedUser()) {
-        String name = ((IdentifiedUser) who).getUserName();
+        String name = who.asIdentifiedUser().getUserName();
         if (name != null && !name.isEmpty()) {
           userName = " (" + name + ")";
         }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 1c70468..cd1c7a3 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
@@ -28,6 +28,7 @@
 import com.google.gerrit.pgm.init.api.InstallPlugins;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -222,6 +223,7 @@
       throw die(err);
     }
 
+    m.add(new GerritServerConfigModule());
     m.add(new InitModule(standalone, initDb));
     m.add(new AbstractModule() {
       @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
index a0c24a6..a11f56f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
@@ -32,6 +32,8 @@
     bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("db2")).to(DB2Initializer.class);
     bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("derby")).to(DerbyInitializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("h2")).to(H2Initializer.class);
     bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("jdbc")).to(JDBCInitializer.class);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java
new file mode 100644
index 0000000..4d710f1
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+
+import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import java.nio.file.Path;
+
+class DerbyInitializer implements DatabaseConfigInitializer {
+
+  private final SitePaths site;
+
+  @Inject
+  DerbyInitializer(final SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    String path = databaseSection.get("database");
+    Path db;
+    if (path == null) {
+      db = site.resolve("db").resolve("ReviewDB");
+      databaseSection.set("database", db.toString());
+    } else {
+      db = site.resolve(path);
+    }
+    if (db == null) {
+      throw die("database.database must be supplied for Derby");
+    }
+    db = db.getParent();
+    FileUtil.mkdirsOrDie(db, "cannot create database.database");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index b670d39..a09dcd5 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm.init;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
@@ -32,7 +34,6 @@
 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;
@@ -153,7 +154,7 @@
       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);
+    String content = new String(Files.readAllBytes(p), UTF_8);
     return new AccountSshKey(new AccountSshKey.Id(id, 0), content);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index a3a3f27..69f2c15 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -61,9 +61,6 @@
     if (auth.getSecure("registerEmailPrivateKey") == null) {
       auth.setSecure("registerEmailPrivateKey", SignedToken.generateRandomKey());
     }
-    if (auth.getSecure("restTokenPrivateKey") == null) {
-      auth.setSecure("restTokenPrivateKey", SignedToken.generateRandomKey());
-    }
 
     initSignedPush();
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
index dfd6171..4659ee3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
@@ -33,7 +33,9 @@
   private void guessDriver(Section database) {
     String url = Strings.emptyToNull(database.get("url"));
     if (url != null && Strings.isNullOrEmpty(database.get("driver"))) {
-      if (url.startsWith("jdbc:h2:")) {
+      if (url.startsWith("jdbc:derby:")) {
+        database.set("driver", "org.apache.derby.jdbc.EmbeddedDriver");
+      } else if (url.startsWith("jdbc:h2:")) {
         database.set("driver", "org.h2.Driver");
       } else if (url.startsWith("jdbc:mysql:")) {
         database.set("driver", "com.mysql.jdbc.Driver");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index 8ae712d..7cc8f10 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm.init;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -120,7 +122,7 @@
       if (in == null) {
         throw new FileNotFoundException("Cannot load resource " + p);
       }
-      try (Reader r = new InputStreamReader(in, "UTF-8")) {
+      try (Reader r = new InputStreamReader(in, UTF_8)) {
         final StringBuilder buf = new StringBuilder();
         final char[] tmp = new char[512];
         int n;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index 9c9843e..e4cc305 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -228,7 +228,7 @@
         for (Path p : paths) {
           String old = p.getFileName().toString();
           String bak = "." + old + ".backup";
-          ui.message("Renaming %s to %s", old, bak);
+          ui.message("Renaming %s to %s\n", old, bak);
           try {
             Files.move(p, p.resolveSibling(bak));
           } catch (IOException e) {
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 21cd3c8..3c91241 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.pgm.init.api.InitUtil.die;
 import static com.google.gerrit.pgm.init.api.InitUtil.savePublic;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
@@ -172,8 +173,8 @@
           return false;
         }
 
-        String n = URLDecoder.decode(pair.substring(0, eq), "UTF-8");
-        String v = URLDecoder.decode(pair.substring(eq + 1), "UTF-8");
+        String n = URLDecoder.decode(pair.substring(0, eq), UTF_8.name());
+        String v = URLDecoder.decode(pair.substring(eq + 1), UTF_8.name());
 
         if ("user".equals(n) || "username".equals(n)) {
           username = v;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 02969d9..e210d5b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.pgm.init.api;
 
-import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
-
 import com.google.gerrit.common.Die;
 
 import java.io.Console;
@@ -218,7 +216,7 @@
           return def;
         }
         for (final T e : options) {
-          if (equalsIgnoreCase(e.toString(), r)) {
+          if (e.toString().equalsIgnoreCase(r)) {
             return e;
           }
         }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 5d09305..b6539f1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -36,12 +36,14 @@
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.RebaseChangeOp;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.config.DisableReverseDnsLookupProvider;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.group.GroupModule;
@@ -106,8 +108,10 @@
     bind(ReplacePatchSetSender.Factory.class).toProvider(
         Providers.<ReplacePatchSetSender.Factory>of(null));
     bind(CurrentUser.class).to(IdentifiedUser.class);
+    factory(BatchUpdate.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchSetInserter.Factory.class);
+    factory(RebaseChangeOp.Factory.class);
 
     bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
       .annotatedWith(GitUploadPackGroups.class)
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index 8a0d9a1..95caf25 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -81,7 +81,7 @@
 
     if (text) {
       root.addAppender(SystemLog.createAppender(logdir, LOG_NAME,
-          new PatternLayout("[%d] %-5p %c %x: %m%n")));
+          new PatternLayout("[%d] [%t] %-5p %c %x: %m%n")));
     }
 
     if (json) {
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index ed11e0f..abcacf9 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -20,7 +20,6 @@
 java_library(
   name = 'lib',
   exported_deps = PLUGIN_API + [
-    '//gerrit-acceptance-tests:lib',
     '//gerrit-antlr:query_exception',
     '//gerrit-antlr:query_parser',
     '//gerrit-common:annotations',
@@ -34,11 +33,13 @@
     '//lib:jsch',
     '//lib:mime-util',
     '//lib:servlet-api-3_1',
+    '//lib:velocity',
     '//lib/commons:lang',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
+    '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/mina:sshd',
   ],
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK
index ec5903a..2ee0e19 100644
--- a/gerrit-plugin-gwtui/BUCK
+++ b/gerrit-plugin-gwtui/BUCK
@@ -55,7 +55,7 @@
   deps = DEPS + [
     '//lib:gwtjsonrpc',
     '//lib:gwtorm_client',
-    '//lib/gwt:dev__jar',
+    '//lib/gwt:dev',
     '//gerrit-gwtui-common:client-lib',
     '//gerrit-common:client',
     '//gerrit-reviewdb:client',
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java
new file mode 100644
index 0000000..528b07a
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugin.client.ui;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.ui.HighlightSuggestion;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A {@code SuggestOracle} for groups. */
+public class GroupSuggestOracle extends SuggestOracle {
+
+  private final int chars;
+
+  /**
+   * @param chars minimum chars to start suggesting.
+   */
+  public GroupSuggestOracle(int chars) {
+    this.chars = chars;
+  }
+
+  @Override
+  public boolean isDisplayStringHTML() {
+    return true;
+  }
+
+  @Override
+  public void requestSuggestions(final Request req, final Callback done) {
+    if (req.getQuery().length() < chars) {
+      responseEmptySuggestion(req, done);
+      return;
+    }
+    RestApi rest = new RestApi("/groups/").addParameter("suggest", req.getQuery());
+    if (req.getLimit() > 0) {
+      rest.addParameter("n", req.getLimit());
+    }
+    rest.get(new AsyncCallback<NativeMap<JavaScriptObject>>() {
+      @Override
+      public void onSuccess(NativeMap<JavaScriptObject> result) {
+        List<String> keys = result.sortedKeys();
+        List<Suggestion> suggestions = new ArrayList<>(keys.size());
+        for (String g : keys) {
+          suggestions.add(new HighlightSuggestion(req.getQuery(), g));
+        }
+        done.onSuggestionsReady(req, new Response(suggestions));
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        responseEmptySuggestion(req, done);
+      }
+    });
+  }
+
+  private static void responseEmptySuggestion(Request req, Callback done) {
+    List<Suggestion> empty = Collections.emptyList();
+    done.onSuggestionsReady(req, new Response(empty));
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 0701771..26d31da 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -125,6 +125,23 @@
       id = newValue;
     }
 
+    public String toRefPrefix() {
+      return refPrefixBuilder().toString();
+    }
+
+    StringBuilder refPrefixBuilder() {
+      StringBuilder r = new StringBuilder(32)
+         .append(REFS_CHANGES);
+      int m = id % 100;
+      if (m < 10) {
+        r.append('0');
+      }
+      return r.append(m)
+          .append('/')
+          .append(id)
+          .append('/');
+    }
+
     /** Parse a Change.Id out of a string representation. */
     public static Id parse(final String str) {
       final Id r = new Id();
@@ -453,6 +470,13 @@
   @Column(id = 17, notNull = false)
   protected String originalSubject;
 
+  /**
+   * Unique id for the changes submitted together assigned during merging.
+   * Only set if the status is MERGED.
+   */
+  @Column(id = 18, notNull = false)
+  protected String submissionId;
+
   protected Change() {
   }
 
@@ -479,6 +503,7 @@
     currentPatchSetId = other.currentPatchSetId;
     subject = other.subject;
     originalSubject = other.originalSubject;
+    submissionId = other.submissionId;
     topic = other.topic;
   }
 
@@ -562,6 +587,14 @@
     }
   }
 
+  public String getSubmissionId() {
+    return submissionId;
+  }
+
+  public void setSubmissionId(String id) {
+    this.submissionId = id;
+  }
+
   public Status getStatus() {
     return Status.forCode(status);
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 4ec957e..ae1b75b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
-
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 
@@ -26,10 +24,20 @@
 /** A single revision of a {@link Change}. */
 public final class PatchSet {
   /** Is the reference name a change reference? */
-  public static boolean isRef(String name) {
+  public static boolean isChangeRef(String name) {
     return Id.fromRef(name) != null;
   }
 
+  /**
+   * Is the reference name a change reference?
+   *
+   * @deprecated use isChangeRef instead.
+   **/
+  @Deprecated
+  public static boolean isRef(String name) {
+    return isChangeRef(name);
+  }
+
   public static String joinGroups(Iterable<String> groups) {
     if (groups == null) {
       return null;
@@ -99,19 +107,9 @@
     }
 
     public String toRefName() {
-      StringBuilder r = new StringBuilder();
-      r.append(REFS_CHANGES);
-      int change = changeId.get();
-      int m = change % 100;
-      if (m < 10) {
-        r.append('0');
-      }
-      r.append(m);
-      r.append('/');
-      r.append(change);
-      r.append('/');
-      r.append(patchSetId);
-      return r.toString();
+      return changeId.refPrefixBuilder()
+          .append(patchSetId)
+          .toString();
     }
 
     /** Parse a PatchSet.Id out of a string representation. */
@@ -189,6 +187,10 @@
   @Column(id = 6, notNull = false)
   protected String groups;
 
+  /** Certificate sent with a push that created this patch set. */
+  @Column(id = 7, notNull = false)
+  protected String pushCertficate;
+
   protected PatchSet() {
   }
 
@@ -248,6 +250,14 @@
     return id.toRefName();
   }
 
+  public String getPushCertificate() {
+    return pushCertficate;
+  }
+
+  public void setPushCertificate(String cert) {
+    pushCertficate = cert;
+  }
+
   @Override
   public String toString() {
     return "[PatchSet " + getId().toString() + "]";
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index ce1b27f..af9e75c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -97,6 +97,7 @@
   protected InheritableBoolean createNewChangeForAllNotInTarget;
 
   protected InheritableBoolean enableSignedPush;
+  protected InheritableBoolean requireSignedPush;
 
   protected Project() {
   }
@@ -111,6 +112,7 @@
     useContentMerge = InheritableBoolean.INHERIT;
     createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
     enableSignedPush = InheritableBoolean.INHERIT;
+    requireSignedPush = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getNameKey() {
@@ -182,6 +184,14 @@
     enableSignedPush = enable;
   }
 
+  public InheritableBoolean getRequireSignedPush() {
+    return requireSignedPush;
+  }
+
+  public void setRequireSignedPush(InheritableBoolean require) {
+    requireSignedPush = require;
+  }
+
   public void setMaxObjectSizeLimit(final String limit) {
     maxObjectSizeLimit = limit;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index da66929..5d2a1fd 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
@@ -21,6 +21,8 @@
 
   public static final String REFS_HEADS = "refs/heads/";
 
+  public static final String REFS_TAGS = "refs/tags/";
+
   public static final String REFS_CHANGES = "refs/changes/";
 
   /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
@@ -62,9 +64,12 @@
   }
 
   public static final String shortName(String ref) {
-    return ref.startsWith(REFS_HEADS)
-        ? ref.substring(REFS_HEADS.length())
-        : ref;
+    if (ref.startsWith(REFS_HEADS)) {
+      return ref.substring(REFS_HEADS.length());
+    } else if (ref.startsWith(REFS_TAGS)) {
+      return ref.substring(REFS_TAGS.length());
+    }
+    return ref;
   }
 
   public static String refsUsers(Account.Id accountId) {
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
index 218d04f..47f409a 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -72,11 +71,19 @@
     assertNotRef("refs/changes/01/1/1/meta");
   }
 
+  @Test
+  public void toRefPrefix() {
+    assertThat(new Change.Id(1).toRefPrefix())
+        .isEqualTo("refs/changes/01/1/");
+    assertThat(new Change.Id(1234).toRefPrefix())
+        .isEqualTo("refs/changes/34/1234/");
+  }
+
   private static void assertRef(int changeId, String refName) {
-    assertEquals(new Change.Id(changeId), Change.Id.fromRef(refName));
+    assertThat(Change.Id.fromRef(refName)).isEqualTo(new Change.Id(changeId));
   }
 
   private static void assertNotRef(String refName) {
-    assertNull(Change.Id.fromRef(refName));
+    assertThat(Change.Id.fromRef(refName)).isNull();
   }
 }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
index 87e5b88..7a6be87 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -83,14 +83,22 @@
     assertThat(joinGroups(ImmutableList.of("", "cd"))).isEqualTo(",cd");
   }
 
+  @Test
+  public void testToRefName() {
+    assertThat(new PatchSet.Id(new Change.Id(1), 23).toRefName())
+        .isEqualTo("refs/changes/01/1/23");
+    assertThat(new PatchSet.Id(new Change.Id(1234), 5).toRefName())
+        .isEqualTo("refs/changes/34/1234/5");
+  }
+
   private static void assertRef(int changeId, int psId, String refName) {
-    assertThat(PatchSet.isRef(refName)).isTrue();
+    assertThat(PatchSet.isChangeRef(refName)).isTrue();
     assertThat(PatchSet.Id.fromRef(refName))
         .isEqualTo(new PatchSet.Id(new Change.Id(changeId), psId));
   }
 
   private static void assertNotRef(String refName) {
-    assertThat(PatchSet.isRef(refName)).isFalse();
+    assertThat(PatchSet.isChangeRef(refName)).isFalse();
     assertThat(PatchSet.Id.fromRef(refName)).isNull();
   }
 }
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 31a9b49..c264779 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -82,6 +82,26 @@
   visibility = ['PUBLIC'],
 )
 
+TESTUTIL_DEPS = [
+  ':server',
+  '//gerrit-common:server',
+  '//gerrit-cache-h2:cache-h2',
+  '//gerrit-extension-api:api',
+  '//gerrit-gpg:gpg',
+  '//gerrit-lucene:lucene',
+  '//gerrit-reviewdb:server',
+  '//lib:gwtorm',
+  '//lib:h2',
+  '//lib:truth',
+  '//lib/guice:guice',
+  '//lib/guice:guice-servlet',
+  '//lib/jgit:jgit',
+  '//lib/jgit:junit',
+  '//lib/log:api',
+  '//lib/log:impl_log4j',
+  '//lib/log:log4j',
+]
+
 TESTUTIL = glob([
   'src/test/java/com/google/gerrit/testutil/**/*.java',
   'src/test/java/com/google/gerrit/server/project/Util.java',
@@ -90,25 +110,9 @@
   name = 'testutil',
   srcs = TESTUTIL,
   deps = [
-    ':server',
-    '//gerrit-common:server',
-    '//gerrit-cache-h2:cache-h2',
-    '//gerrit-extension-api:api',
-    '//gerrit-gpg:gpg',
-    '//gerrit-lucene:lucene',
-    '//gerrit-reviewdb:server',
-    '//lib:gwtorm',
-    '//lib:h2',
-    '//lib:truth',
     '//lib/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',
   ],
+  provided_deps = TESTUTIL_DEPS,
   exported_deps = [
     '//lib/easymock:easymock',
     '//lib/powermock:powermock-api-easymock',
@@ -147,19 +151,10 @@
   name = 'prolog_tests',
   srcs = PROLOG_TESTS,
   resources = glob(['src/test/resources/com/google/gerrit/rules/**/*']),
-  deps = [
+  deps = TESTUTIL_DEPS + [
     ':prolog_test_case',
-    ':server',
     ':testutil',
-    '//gerrit-common:server',
-    '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:junit',
-    '//lib:truth',
-    '//lib/jgit:jgit',
-    '//lib/guice:guice',
     '//lib/prolog:runtime',
   ],
 )
@@ -171,22 +166,13 @@
 java_test(
   name = 'query_tests',
   srcs = QUERY_TESTS,
-  deps = [
-    ':server',
+  deps = TESTUTIL_DEPS + [
     ':testutil',
     '//gerrit-antlr:query_exception',
     '//gerrit-antlr:query_parser',
     '//gerrit-common:annotations',
-    '//gerrit-common:server',
-    '//gerrit-extension-api:api',
-    '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
-    '//lib:gwtorm',
-    '//lib:truth',
     '//lib/antlr:java_runtime',
-    '//lib/guice:guice',
-    '//lib/jgit:jgit',
-    '//lib/jgit:junit',
     '//lib/joda:joda-time',
   ],
   source_under_test = [':server'],
@@ -199,25 +185,15 @@
     ['src/test/java/**/*.java'],
     excludes = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS
   ),
-  deps = [
-    ':server',
+  deps = TESTUTIL_DEPS + [
     ':testutil',
     '//gerrit-antlr:query_exception',
     '//gerrit-common:annotations',
-    '//gerrit-common:server',
-    '//gerrit-extension-api:api',
-    '//gerrit-gpg:gpg',
-    '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
     '//lib:args4j',
     '//lib:grappa',
     '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:truth',
-    '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
-    '//lib/jgit:jgit',
-    '//lib/jgit:junit',
     '//lib/joda:joda-time',
     '//lib/prolog:runtime',
   ],
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/Version.java b/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
index 641ba03..57a2946 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.common;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,7 +41,7 @@
       if (in == null) {
         return "(dev)";
       }
-      try (BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8"))) {
+      try (BufferedReader r = new BufferedReader(new InputStreamReader(in, UTF_8))) {
         String vs = r.readLine();
         if (vs != null && vs.startsWith("v")) {
           vs = vs.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index b3cf660..1d1f571 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.change.ChangeKind.NO_CHANGE;
 import static com.google.gerrit.server.change.ChangeKind.NO_CODE_CHANGE;
 import static com.google.gerrit.server.change.ChangeKind.TRIVIAL_REBASE;
@@ -45,6 +46,7 @@
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.NavigableSet;
 import java.util.Objects;
@@ -86,16 +88,22 @@
 
   Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet.Id psId) throws OrmException {
-    return getForPatchSet(db, ctl, db.patchSets().get(psId));
+    PatchSet ps = db.patchSets().get(psId);
+    if (ps == null) {
+      return Collections.emptyList();
+    }
+    return getForPatchSet(db, ctl, ps);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet ps) throws OrmException {
+    checkNotNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(db, ctl);
     try {
       ProjectState project =
           projectCache.checkedGet(cd.change().getDest().getParentKey());
       ListMultimap<PatchSet.Id, PatchSetApproval> all = cd.approvals();
+      checkNotNull(all, "all should not be null");
 
       Table<String, Account.Id, PatchSetApproval> byUser =
           HashBasedTable.create();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 7265196..31058bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -222,9 +222,8 @@
   }
 
   public void addApprovals(ReviewDb db, ChangeUpdate update,
-      LabelTypes labelTypes, PatchSet ps, PatchSetInfo info,
-      ChangeControl changeCtl, Map<String, Short> approvals)
-      throws OrmException {
+      LabelTypes labelTypes, PatchSet ps, ChangeControl changeCtl,
+      Map<String, Short> approvals) throws OrmException {
     if (!approvals.isEmpty()) {
       checkApprovals(approvals, changeCtl);
       List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
@@ -233,7 +232,7 @@
         LabelType lt = labelTypes.byLabel(vote.getKey());
         cells.add(new PatchSetApproval(new PatchSetApproval.Key(
             ps.getId(),
-            info.getCommitter().getAccount(),
+            ps.getUploader(),
             lt.getLabelId()),
             vote.getValue(),
             ts));
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 590d065..0de7e38 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
@@ -22,8 +22,8 @@
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.Project;
@@ -32,21 +32,18 @@
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeMessages;
 import com.google.gerrit.server.change.ChangeTriplet;
-import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.server.util.MagicBranch;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -193,8 +190,7 @@
     return subject;
   }
 
-  private final Provider<CurrentUser> userProvider;
-  private final CommitValidators.Factory commitValidatorsFactory;
+  private final Provider<IdentifiedUser> user;
   private final Provider<ReviewDb> db;
   private final Provider<InternalChangeQuery> queryProvider;
   private final RevertedSender.Factory revertedSenderFactory;
@@ -202,19 +198,19 @@
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final ChangeIndexer indexer;
+  private final BatchUpdate.Factory updateFactory;
 
   @Inject
-  ChangeUtil(Provider<CurrentUser> userProvider,
-      CommitValidators.Factory commitValidatorsFactory,
+  ChangeUtil(Provider<IdentifiedUser> user,
       Provider<ReviewDb> db,
       Provider<InternalChangeQuery> queryProvider,
       RevertedSender.Factory revertedSenderFactory,
       ChangeInserter.Factory changeInserterFactory,
       GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated,
-      ChangeIndexer indexer) {
-    this.userProvider = userProvider;
-    this.commitValidatorsFactory = commitValidatorsFactory;
+      ChangeIndexer indexer,
+      BatchUpdate.Factory updateFactory) {
+    this.user = user;
     this.db = db;
     this.queryProvider = queryProvider;
     this.revertedSenderFactory = revertedSenderFactory;
@@ -222,13 +218,14 @@
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
     this.indexer = indexer;
+    this.updateFactory = updateFactory;
   }
 
   public Change.Id revert(ChangeControl ctl, PatchSet.Id patchSetId,
-      String message, PersonIdent myIdent, SshInfo sshInfo)
+      String message, PersonIdent myIdent)
       throws NoSuchChangeException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
-      InvalidChangeOperationException {
+      RestApiException, UpdateException {
     Change.Id changeId = patchSetId.getParentKey();
     PatchSet patch = db.get().patchSets().get(patchSetId);
     if (patch == null) {
@@ -242,8 +239,8 @@
       RevCommit commitToRevert =
           revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
 
-      PersonIdent authorIdent =
-          user().newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
+      PersonIdent authorIdent = user.get()
+          .newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
 
       RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
       revWalk.parseHeaders(parentToCommitToRevert);
@@ -267,73 +264,49 @@
           ChangeIdUtil.insertId(message, computedChangeId, true));
 
       RevCommit revertCommit;
+      ChangeInserter ins;
       try (ObjectInserter oi = git.newObjectInserter()) {
         ObjectId id = oi.insert(revertCommitBuilder);
         oi.flush();
         revertCommit = revWalk.parseCommit(id);
+
+        RefControl refControl = ctl.getRefControl();
+        Change change = new Change(
+            new Change.Key("I" + computedChangeId.name()),
+            new Change.Id(db.get().nextChangeId()),
+            user.get().getAccountId(),
+            changeToRevert.getDest(),
+            TimeUtil.nowTs());
+        change.setTopic(changeToRevert.getTopic());
+        ins = changeInserterFactory.create(
+              refControl, change, revertCommit)
+            .setValidatePolicy(CommitValidators.Policy.GERRIT);
+        StringBuilder msgBuf = new StringBuilder();
+        msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
+        msgBuf.append("\n\n");
+        msgBuf.append("This patchset was reverted in change: ")
+              .append(change.getKey().get());
+        ins.setMessage(msgBuf.toString());
+        try (BatchUpdate bu = updateFactory.create(
+            db.get(), change.getProject(), refControl.getUser(),
+            change.getCreatedOn())) {
+          bu.setRepository(git, revWalk, oi);
+          bu.insertChange(ins);
+          bu.execute();
+        }
       }
 
-      RefControl refControl = ctl.getRefControl();
-      Change change = new Change(
-          new Change.Key("I" + computedChangeId.name()),
-          new Change.Id(db.get().nextChangeId()),
-          user().getAccountId(),
-          changeToRevert.getDest(),
-          TimeUtil.nowTs());
-      change.setTopic(changeToRevert.getTopic());
-      ChangeInserter ins =
-          changeInserterFactory.create(refControl.getProjectControl(),
-              change, revertCommit);
-      PatchSet ps = ins.getPatchSet();
-
-      String ref = refControl.getRefName();
-      String cmdRef = MagicBranch.NEW_PUBLISH_CHANGE
-          + ref.substring(ref.lastIndexOf('/') + 1);
-      CommitReceivedEvent commitReceivedEvent = new CommitReceivedEvent(
-          new ReceiveCommand(ObjectId.zeroId(), revertCommit.getId(), cmdRef),
-          refControl.getProjectControl().getProject(),
-          refControl.getRefName(), revertCommit, user());
-
+      Change.Id id = ins.getChange().getId();
       try {
-        commitValidatorsFactory.create(refControl, sshInfo, git)
-            .validateForGerritCommits(commitReceivedEvent);
-      } catch (CommitValidationException e) {
-        throw new InvalidChangeOperationException(e.getMessage());
-      }
-
-      RefUpdate ru = git.updateRef(ps.getRefName());
-      ru.setExpectedOldObjectId(ObjectId.zeroId());
-      ru.setNewObjectId(revertCommit);
-      ru.disableRefLog();
-      if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-        throw new IOException(String.format(
-            "Failed to create ref %s in %s: %s", ps.getRefName(),
-            change.getDest().getParentKey().get(), ru.getResult()));
-      }
-
-      ChangeMessage cmsg = new ChangeMessage(
-          new ChangeMessage.Key(changeId, messageUUID(db.get())),
-          user().getAccountId(), TimeUtil.nowTs(), patchSetId);
-      StringBuilder msgBuf = new StringBuilder();
-      msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
-      msgBuf.append("\n\n");
-      msgBuf.append("This patchset was reverted in change: ")
-            .append(change.getKey().get());
-      cmsg.setMessage(msgBuf.toString());
-
-      ins.setMessage(cmsg).insert();
-
-      try {
-        RevertedSender cm = revertedSenderFactory.create(change.getId());
-        cm.setFrom(user().getAccountId());
-        cm.setChangeMessage(cmsg);
+        RevertedSender cm = revertedSenderFactory.create(id);
+        cm.setFrom(user.get().getAccountId());
+        cm.setChangeMessage(ins.getChangeMessage());
         cm.send();
       } catch (Exception err) {
-        log.error("Cannot send email for revert change " + change.getId(),
-            err);
+        log.error("Cannot send email for revert change " + id, err);
       }
 
-      return change.getId();
+      return id;
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(changeId, e);
     }
@@ -476,10 +449,6 @@
     throw new ResourceNotFoundException(id);
   }
 
-  private IdentifiedUser user() {
-    return (IdentifiedUser) userProvider.get();
-  }
-
   private static void deleteOnlyDraftPatchSetPreserveRef(ReviewDb db,
       PatchSet patch) throws NoSuchChangeException, OrmException {
     PatchSet.Id patchSetId = patch.getId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 4f2c6b9..6a8600f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
@@ -100,4 +101,21 @@
   public boolean isIdentifiedUser() {
     return false;
   }
+
+  /** Cast to IdentifiedUser if possible. */
+  public IdentifiedUser asIdentifiedUser() {
+    throw new UnsupportedOperationException(
+        getClass().getSimpleName() + " is not an IdentifiedUser");
+  }
+
+  /** Return account ID if {@link #isIdentifiedUser} is true. */
+  public Account.Id getAccountId() {
+    throw new UnsupportedOperationException(
+        getClass().getSimpleName() + " is not an IdentifiedUser");
+  }
+
+  /** Check if the CurrentUser is an InternalUser. */
+  public boolean isInternalUser() {
+    return false;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index baba4bb..df25042 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
@@ -39,7 +39,6 @@
 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;
@@ -255,7 +254,12 @@
     return state;
   }
 
-  /** The account identity for the user. */
+  @Override
+  public IdentifiedUser asIdentifiedUser() {
+    return this;
+  }
+
+  @Override
   public Account.Id getAccountId() {
     return accountId;
   }
@@ -334,8 +338,11 @@
       try {
         starredChanges = starredChangeIds(
             starredQuery != null ? starredQuery : starredQuery());
-      } catch (OrmException | OrmRuntimeException e) {
+      } catch (OrmException | RuntimeException e) {
         log.warn("Cannot query starred changes", e);
+        starredChanges = Collections.emptySet();
+      } finally {
+        starredQuery = null;
       }
     }
     return starredChanges;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index ef28ed8..d0c2dc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -62,6 +62,11 @@
   }
 
   @Override
+  public boolean isInternalUser() {
+    return true;
+  }
+
+  @Override
   public String toString() {
     return "InternalUser";
   }
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 15519cc..7b182b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -68,7 +68,7 @@
  */
 @Singleton
 public class PatchLineCommentsUtil {
-  public static Ordering<PatchLineComment> PLC_ORDER =
+  public static final Ordering<PatchLineComment> PLC_ORDER =
       new Ordering<PatchLineComment>() {
     @Override
     public int compare(PatchLineComment c1, PatchLineComment c2) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index 825bd3b..aad427b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -61,19 +61,19 @@
 
   private final AccountsSection accountsSection;
   private final GroupControl.Factory groupControlFactory;
-  private final CurrentUser currentUser;
+  private final CurrentUser user;
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountVisibility accountVisibility;
 
   AccountControl(final ProjectCache projectCache,
         final GroupControl.Factory groupControlFactory,
-        final CurrentUser currentUser,
+        final CurrentUser user,
         final IdentifiedUser.GenericFactory userFactory,
         final AccountVisibility accountVisibility) {
     this.accountsSection =
         projectCache.getAllProjects().getConfig().getAccountsSection();
     this.groupControlFactory = groupControlFactory;
-    this.currentUser = currentUser;
+    this.user = user;
     this.userFactory = userFactory;
     this.accountVisibility = accountVisibility;
   }
@@ -100,11 +100,10 @@
    */
   public boolean canSee(final Account.Id otherUser) {
     // Special case: I can always see myself.
-    if (currentUser.isIdentifiedUser()
-        && ((IdentifiedUser) currentUser).getAccountId().equals(otherUser)) {
+    if (user.isIdentifiedUser() && user.getAccountId().equals(otherUser)) {
       return true;
     }
-    if (currentUser.getCapabilities().canViewAllAccounts()) {
+    if (user.getCapabilities().canViewAllAccounts()) {
       return true;
     }
 
@@ -119,7 +118,7 @@
           }
         }
 
-        if (currentUser.getEffectiveGroups().containsAnyOf(usersGroups)) {
+        if (user.getEffectiveGroups().containsAnyOf(usersGroups)) {
           return true;
         }
         break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 66c7570..7de3d64 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -106,7 +106,7 @@
     try {
       try (ReviewDb db = schema.open()) {
         AccountExternalId.Key key = id(who);
-        AccountExternalId id = db.accountExternalIds().get(key);
+        AccountExternalId id = getAccountExternalId(db, key);
         if (id == null) {
           // New account, automatically create and return.
           //
@@ -130,6 +130,29 @@
     }
   }
 
+  private AccountExternalId getAccountExternalId(ReviewDb db,
+      AccountExternalId.Key key) throws OrmException {
+    String keyValue = key.get();
+    String keyScheme = keyValue.substring(0, keyValue.indexOf(':') + 1);
+
+    // We don't have at the moment an account_by_external_id cache
+    // but by using the accounts cache we get the list of external_ids
+    // without having to query the DB every time
+    if (keyScheme.equals(AccountExternalId.SCHEME_GERRIT)
+        || keyScheme.equals(AccountExternalId.SCHEME_USERNAME)) {
+      AccountState state = byIdCache.getByUsername(
+          keyValue.substring(keyScheme.length()));
+      if (state != null) {
+        for (AccountExternalId accountExternalId : state.getExternalIds()) {
+          if (accountExternalId.getKey().equals(key)) {
+            return accountExternalId;
+          }
+        }
+      }
+    }
+    return db.accountExternalIds().get(key);
+  }
+
   private void update(ReviewDb db, AuthRequest who, AccountExternalId extId)
       throws OrmException, NameAlreadyUsedException, InvalidUserNameException {
     IdentifiedUser user = userFactory.create(extId.getAccountId());
@@ -152,12 +175,14 @@
     }
 
     if (!realm.allowsEdit(Account.FieldName.FULL_NAME)
+        && who.getDisplayName() != null
         && !eq(user.getAccount().getFullName(), who.getDisplayName())) {
       toUpdate = load(toUpdate, user.getAccountId(), db);
       toUpdate.setFullName(who.getDisplayName());
     }
 
     if (!realm.allowsEdit(Account.FieldName.USER_NAME)
+        && who.getUserName() != null
         && !eq(user.getUserName(), who.getUserName())) {
       changeUserNameFactory.create(db, user, who.getUserName()).call();
     }
@@ -322,7 +347,7 @@
       who = realm.link(db, to, who);
 
       AccountExternalId.Key key = id(who);
-      AccountExternalId extId = db.accountExternalIds().get(key);
+      AccountExternalId extId = getAccountExternalId(db, key);
       if (extId != null) {
         if (!extId.getAccountId().equals(to)) {
           throw new AccountException("Identity in use by another account");
@@ -413,7 +438,7 @@
       who = realm.unlink(db, from, who);
 
       AccountExternalId.Key key = id(who);
-      AccountExternalId extId = db.accountExternalIds().get(key);
+      AccountExternalId extId = getAccountExternalId(db, key);
       if (extId != null) {
         if (!extId.getAccountId().equals(from)) {
           throw new AccountException("Identity in use by another account");
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 80a451a..80ea907 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -114,7 +114,7 @@
     if (id.equals("self")) {
       CurrentUser user = self.get();
       if (user.isIdentifiedUser()) {
-        return (IdentifiedUser) user;
+        return user.asIdentifiedUser();
       } else if (user instanceof AnonymousUser) {
         throw new AuthException("Authentication required");
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index a8eec10..7bb00c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -58,7 +58,7 @@
   }
 
   /** Identity of the user the control will compute for. */
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
new file mode 100644
index 0000000..e6e7644
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+@Singleton
+public class GetEditPreferences implements RestReadView<AccountResource> {
+  private final Provider<CurrentUser> self;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
+
+  @Inject
+  GetEditPreferences(Provider<CurrentUser> self,
+      AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
+    this.self = self;
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
+  }
+
+  @Override
+  public EditPreferencesInfo apply(AccountResource rsrc) throws AuthException,
+      IOException, ConfigInvalidException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
+    }
+
+    try (Repository git = gitMgr.openRepository(allUsersName)) {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forUser(rsrc.getUser().getAccountId());
+      p.load(git);
+
+      return loadSection(p.getConfig(), UserConfigSections.EDIT, null,
+          new EditPreferencesInfo(), EditPreferencesInfo.defaults());
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
index 3df6837..08bf83e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -51,7 +52,6 @@
 public class GetPreferences implements RestReadView<AccountResource> {
   private static final Logger log = LoggerFactory.getLogger(GetPreferences.class);
 
-  public static final String MY = "my";
   public static final String KEY_URL = "url";
   public static final String KEY_TARGET = "target";
   public static final String KEY_ID = "id";
@@ -165,7 +165,7 @@
     private List<TopMenu.MenuItem> my(VersionedAccountPreferences v) {
       List<TopMenu.MenuItem> my = new ArrayList<>();
       Config cfg = v.getConfig();
-      for (String subsection : cfg.getSubsections(MY)) {
+      for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
         String url = my(cfg, subsection, KEY_URL, "#/");
         String target = my(cfg, subsection, KEY_TARGET,
             url.startsWith("#") ? null : "_blank");
@@ -178,7 +178,7 @@
 
     private static String my(Config cfg, String subsection, String key,
         String defaultValue) {
-      String val = cfg.getString(MY, subsection, key);
+      String val = cfg.getString(UserConfigSections.MY, subsection, key);
       return !Strings.isNullOrEmpty(val) ? val : defaultValue;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 084cfe8..2e03913 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -20,8 +20,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.InternalUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -122,7 +120,7 @@
     return group;
   }
 
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
@@ -132,7 +130,7 @@
      * for visibility of all groups that are not an internal group to
      * server administrators.
      */
-    return user instanceof InternalUser
+    return user.isInternalUser()
       || groupBackend.isVisibleToAll(group.getGroupUUID())
       || user.getEffectiveGroups().contains(group.getGroupUUID())
       || user.getCapabilities().canAdministrateServer()
@@ -145,8 +143,8 @@
       isOwner = false;
     } else if (isOwner == null) {
       AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
-      isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
-             || getCurrentUser().getCapabilities().canAdministrateServer();
+      isOwner = getUser().getEffectiveGroups().contains(ownerUUID)
+             || getUser().getCapabilities().canAdministrateServer();
     }
     return isOwner;
   }
@@ -160,8 +158,7 @@
   }
 
   public boolean canSeeMember(Account.Id id) {
-    if (user.isIdentifiedUser()
-        && ((IdentifiedUser) user).getAccountId().equals(id)) {
+    if (user.isIdentifiedUser() && user.getAccountId().equals(id)) {
       return true;
     }
     return canSeeMembers();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 7cf1e37..54d4cc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -70,6 +70,8 @@
     put(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
     get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
     put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
+    get(ACCOUNT_KIND, "preferences.edit").to(GetEditPreferences.class);
+    put(ACCOUNT_KIND, "preferences.edit").to(SetEditPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
 
     child(ACCOUNT_KIND, "starred.changes").to(StarredChanges.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
new file mode 100644
index 0000000..2df1a77
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
+@Singleton
+public class SetEditPreferences implements
+    RestModifyView<AccountResource, EditPreferencesInfo> {
+
+  private final Provider<CurrentUser> self;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  SetEditPreferences(Provider<CurrentUser> self,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      AllUsersName allUsersName) {
+    this.self = self;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  public Response<?> apply(AccountResource rsrc, EditPreferencesInfo in)
+      throws AuthException, BadRequestException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
+    }
+
+    if (in == null) {
+      throw new BadRequestException("input must be provided");
+    }
+
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
+
+    VersionedAccountPreferences prefs;
+    try {
+      prefs = VersionedAccountPreferences.forUser(accountId);
+      prefs.load(md);
+      storeSection(prefs.getConfig(), UserConfigSections.EDIT, null, in,
+          EditPreferencesInfo.defaults());
+      prefs.commit(md);
+    } finally {
+      md.close();
+    }
+
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index c7725c5..d355dae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -19,7 +19,6 @@
 import static com.google.gerrit.server.account.GetPreferences.KEY_TARGET;
 import static com.google.gerrit.server.account.GetPreferences.KEY_TOKEN;
 import static com.google.gerrit.server.account.GetPreferences.KEY_URL;
-import static com.google.gerrit.server.account.GetPreferences.MY;
 import static com.google.gerrit.server.account.GetPreferences.URL_ALIAS;
 
 import com.google.common.base.Strings;
@@ -42,6 +41,7 @@
 import com.google.gerrit.server.account.SetPreferences.Input;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -192,7 +192,7 @@
       List<TopMenu.MenuItem> my) {
     Config cfg = prefs.getConfig();
     if (my != null) {
-      unsetSection(cfg, MY);
+      unsetSection(cfg, UserConfigSections.MY);
       for (TopMenu.MenuItem item : my) {
         set(cfg, item.name, KEY_URL, item.url);
         set(cfg, item.name, KEY_TARGET, item.target);
@@ -203,9 +203,9 @@
 
   private static void set(Config cfg, String section, String key, String val) {
     if (Strings.isNullOrEmpty(val)) {
-      cfg.unset(MY, section, key);
+      cfg.unset(UserConfigSections.MY, section, key);
     } else {
-      cfg.setString(MY, section, key, val);
+      cfg.setString(UserConfigSections.MY, section, key, val);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
index 5e65acd..426c6f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -24,7 +24,6 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.FileMode;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
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 3f578ae..7be8299 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.SuggestAccounts;
@@ -66,7 +65,7 @@
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    return api.create(new AccountResource((IdentifiedUser)self.get()));
+    return api.create(new AccountResource(self.get().asIdentifiedUser()));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
index 9faa418..7d65ce9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
@@ -16,9 +16,11 @@
 
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.GpgException;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 
 import java.util.List;
@@ -27,8 +29,13 @@
 public interface GpgApiAdapter {
   Map<String, GpgKeyInfo> listGpgKeys(AccountResource account)
       throws RestApiException, GpgException;
+
   Map<String, GpgKeyInfo> putGpgKeys(AccountResource account, List<String> add,
       List<String> delete) throws RestApiException, GpgException;
+
   GpgKeyApi gpgKey(AccountResource account, IdString idStr)
       throws RestApiException, GpgException;
+
+  PushCertificateInfo checkPushCertificate(String certStr,
+      IdentifiedUser expectedUser) throws GpgException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 868094e..b55642c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.api.changes;
 
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -33,7 +32,6 @@
 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;
@@ -51,7 +49,7 @@
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.change.SubmittedTogether;
 import com.google.gerrit.server.change.SuggestReviewers;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -166,7 +164,7 @@
   public void abandon(AbandonInput in) throws RestApiException {
     try {
       abandon.apply(change, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | UpdateException e) {
       throw new RestApiException("Cannot abandon change", e);
     }
   }
@@ -180,7 +178,7 @@
   public void restore(RestoreInput in) throws RestApiException {
     try {
       restore.apply(change, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | UpdateException e) {
       throw new RestApiException("Cannot restore change", e);
     }
   }
@@ -194,7 +192,7 @@
   public ChangeApi revert(RevertInput in) throws RestApiException {
     try {
       return changeApi.id(revert.apply(change, in)._number);
-    } catch (OrmException | EmailException | IOException e) {
+    } catch (OrmException | IOException | UpdateException e) {
       throw new RestApiException("Cannot revert change", e);
     }
   }
@@ -219,7 +217,7 @@
     in.topic = topic;
     try {
       putTopic.apply(change, in);
-    } catch (OrmException | IOException e) {
+    } catch (UpdateException e) {
       throw new RestApiException("Cannot set topic", e);
     }
   }
@@ -235,7 +233,7 @@
   public void addReviewer(AddReviewerInput in) throws RestApiException {
     try {
       postReviewers.apply(change, in);
-    } catch (OrmException | EmailException | IOException e) {
+    } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot add change reviewer", e);
     }
   }
@@ -274,7 +272,7 @@
     try {
       CurrentUser u = user.get();
       if (u.isIdentifiedUser()) {
-        ((IdentifiedUser) u).clearStarredChanges();
+        u.asIdentifiedUser().clearStarredChanges();
       }
       return changeJson.create(s).format(change);
     } catch (OrmException e) {
@@ -292,7 +290,7 @@
     try {
       Response<EditInfo> edit = editDetail.apply(change);
       return edit.isNone() ? null : edit.value();
-    } catch (IOException | OrmException | InvalidChangeOperationException e) {
+    } catch (IOException | OrmException e) {
       throw new RestApiException("Cannot retrieve change edit", e);
     }
   }
@@ -306,7 +304,7 @@
   public void setHashtags(HashtagsInput input) throws RestApiException {
     try {
       postHashtags.apply(change, input);
-    } catch (IOException | OrmException e) {
+    } catch (RestApiException | UpdateException e) {
       throw new RestApiException("Cannot post hashtags", e);
     }
   }
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 f7705a7..acda1ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -29,9 +29,9 @@
 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.git.UpdateException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
@@ -95,7 +95,8 @@
           TopLevelResource.INSTANCE, in).value();
       return api.create(changes.parse(TopLevelResource.INSTANCE,
           IdString.fromUrl(out.changeId)));
-    } catch (OrmException | IOException | InvalidChangeOperationException e) {
+    } catch (OrmException | IOException | InvalidChangeOperationException
+        | UpdateException e) {
       throw new RestApiException("Cannot create change", e);
     }
   }
@@ -129,7 +130,7 @@
     try {
       CurrentUser u = user.get();
       if (u.isIdentifiedUser()) {
-        ((IdentifiedUser) u).clearStarredChanges();
+        u.asIdentifiedUser().clearStarredChanges();
       }
       List<?> result = qc.apply(TopLevelResource.INSTANCE);
       if (result.isEmpty()) {
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 0569e5f..0926142 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -50,10 +50,11 @@
 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.RebaseUtil;
 import com.google.gerrit.server.change.Reviewed;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -73,7 +74,7 @@
   private final CherryPick cherryPick;
   private final DeleteDraftPatchSet deleteDraft;
   private final Rebase rebase;
-  private final RebaseChange rebaseChange;
+  private final RebaseUtil rebaseUtil;
   private final Submit submit;
   private final PublishDraftPatchSet publish;
   private final Reviewed.PutReviewed putReviewed;
@@ -99,7 +100,7 @@
       CherryPick cherryPick,
       DeleteDraftPatchSet deleteDraft,
       Rebase rebase,
-      RebaseChange rebaseChange,
+      RebaseUtil rebaseUtil,
       Submit submit,
       PublishDraftPatchSet publish,
       Reviewed.PutReviewed putReviewed,
@@ -123,7 +124,7 @@
     this.cherryPick = cherryPick;
     this.deleteDraft = deleteDraft;
     this.rebase = rebase;
-    this.rebaseChange = rebaseChange;
+    this.rebaseUtil = rebaseUtil;
     this.review = review;
     this.submit = submit;
     this.publish = publish;
@@ -149,7 +150,7 @@
   public void review(ReviewInput in) throws RestApiException {
     try {
       review.get().apply(revision, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | UpdateException e) {
       throw new RestApiException("Cannot post review", e);
     }
   }
@@ -173,7 +174,7 @@
   public void publish() throws RestApiException {
     try {
       publish.apply(revision, new PublishDraftPatchSet.Input());
-    } catch (OrmException | IOException e) {
+    } catch (UpdateException e) {
       throw new RestApiException("Cannot publish draft patch set", e);
     }
   }
@@ -197,21 +198,21 @@
   public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
       return changes.id(rebase.apply(revision, in)._number);
-    } catch (OrmException | EmailException | IOException e) {
+    } catch (OrmException | EmailException | UpdateException | IOException e) {
       throw new RestApiException("Cannot rebase ps", e);
     }
   }
 
   @Override
   public boolean canRebase() {
-    return rebaseChange.canRebase(revision);
+    return rebaseUtil.canRebase(revision);
   }
 
   @Override
   public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
     try {
       return changes.id(cherryPick.apply(revision, in)._number);
-    } catch (OrmException | EmailException | IOException e) {
+    } catch (OrmException | IOException | UpdateException e) {
       throw new RestApiException("Cannot cherry pick", e);
     }
   }
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
index 97ba5d3..3d2c960 100644
--- 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
@@ -138,6 +138,7 @@
     list.setLimit(req.getLimit());
     list.setStart(req.getStart());
     list.setMatchSubstring(req.getSubstring());
+    list.setSuggest(req.getSuggest());
     try {
       return list.apply(tlr);
     } catch (OrmException e) {
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 3113d07..975e6c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
@@ -23,6 +23,7 @@
     bind(Projects.class).to(ProjectsImpl.class);
 
     factory(BranchApiImpl.Factory.class);
+    factory(TagApiImpl.Factory.class);
     factory(ProjectApiImpl.Factory.class);
     factory(ChildProjectApiImpl.Factory.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 84b219b5..dbd246c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
+import com.google.gerrit.extensions.api.projects.TagApi;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -35,6 +37,7 @@
 import com.google.gerrit.server.project.GetDescription;
 import com.google.gerrit.server.project.ListBranches;
 import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ListTags;
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
@@ -66,7 +69,9 @@
   private final ProjectJson projectJson;
   private final String name;
   private final BranchApiImpl.Factory branchApi;
+  private final TagApiImpl.Factory tagApi;
   private final Provider<ListBranches> listBranchesProvider;
+  private final Provider<ListTags> listTagsProvider;
 
   @AssistedInject
   ProjectApiImpl(Provider<CurrentUser> user,
@@ -79,11 +84,13 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
       Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       @Assisted ProjectResource project) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        listBranchesProvider, project, null);
+        tagApiFactory, listBranchesProvider, listTagsProvider, project, null);
   }
 
   @AssistedInject
@@ -97,11 +104,13 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
       Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       @Assisted String name) {
     this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        listBranchesProvider, null, name);
+        tagApiFactory, listBranchesProvider, listTagsProvider, null, name);
   }
 
   private ProjectApiImpl(Provider<CurrentUser> user,
@@ -114,7 +123,9 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
       Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       ProjectResource project,
       String name) {
     this.user = user;
@@ -129,7 +140,9 @@
     this.project = project;
     this.name = name;
     this.branchApi = branchApiFactory;
+    this.tagApi = tagApiFactory;
     this.listBranchesProvider = listBranchesProvider;
+    this.listTagsProvider = listTagsProvider;
   }
 
   @Override
@@ -179,8 +192,8 @@
   }
 
   @Override
-  public ListBranchesRequest branches() {
-    return new ListBranchesRequest() {
+  public ListRefsRequest<BranchInfo> branches() {
+    return new ListRefsRequest<BranchInfo>() {
       @Override
       public List<BranchInfo> get() throws RestApiException {
         return listBranches(this);
@@ -188,7 +201,7 @@
     };
   }
 
-  private List<BranchInfo> listBranches(ListBranchesRequest request)
+  private List<BranchInfo> listBranches(ListRefsRequest<BranchInfo> request)
       throws RestApiException {
     ListBranches list = listBranchesProvider.get();
     list.setLimit(request.getLimit());
@@ -203,6 +216,30 @@
   }
 
   @Override
+  public ListRefsRequest<TagInfo> tags() {
+    return new ListRefsRequest<TagInfo>() {
+      @Override
+      public List<TagInfo> get() throws RestApiException {
+        return listTags(this);
+      }
+    };
+  }
+
+  private List<TagInfo> listTags(ListRefsRequest<TagInfo> request)
+      throws RestApiException {
+    ListTags list = listTagsProvider.get();
+    list.setLimit(request.getLimit());
+    list.setStart(request.getStart());
+    list.setMatchSubstring(request.getSubstring());
+    list.setMatchRegex(request.getRegex());
+    try {
+      return list.apply(checkExists());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot list tags", e);
+    }
+  }
+
+  @Override
   public List<ProjectInfo> children() throws RestApiException {
     return children(false);
   }
@@ -229,6 +266,11 @@
     return branchApi.create(checkExists(), ref);
   }
 
+  @Override
+  public TagApi tag(String ref) throws ResourceNotFoundException {
+    return tagApi.create(checkExists(), ref);
+  }
+
   private ProjectResource checkExists() throws ResourceNotFoundException {
     if (project == null) {
       throw new ResourceNotFoundException(name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
new file mode 100644
index 0000000..086447d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.projects;
+
+import com.google.gerrit.extensions.api.projects.TagApi;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.ListTags;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+
+public class TagApiImpl implements TagApi {
+  interface Factory {
+    TagApiImpl create(ProjectResource project, String ref);
+  }
+
+  private final ListTags listTags;
+  private final String ref;
+  private final ProjectResource project;
+
+  @Inject
+  TagApiImpl(ListTags listTags,
+      @Assisted ProjectResource project,
+      @Assisted String ref) {
+    this.listTags = listTags;
+    this.project = project;
+    this.ref = ref;
+  }
+
+  @Override
+  public TagInfo get() throws RestApiException {
+    try {
+      return listTags.get(project, IdString.fromDecoded(ref));
+    } catch (IOException e) {
+      throw new RestApiException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 0c09155..bd4a3b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -126,7 +126,7 @@
     String groupDn = uuid.get().substring(LDAP_UUID.length());
     CurrentUser user = userProvider.get();
     if (!(user.isIdentifiedUser())
-        || !membershipsOf((IdentifiedUser) user).contains(uuid)) {
+        || !membershipsOf(user.asIdentifiedUser()).contains(uuid)) {
       try {
         if (!existsCache.get(groupDn)) {
           return null;
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 27779f2..9d7f7b7 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
@@ -163,6 +163,7 @@
       return null;
 
     } else {
+      checkBackendCompliance(n, v[0], Strings.isNullOrEmpty(d));
       return v[0];
     }
   }
@@ -186,6 +187,16 @@
     }
   }
 
+  private static void checkBackendCompliance(String configOption,
+      String suppliedValue, boolean disabledByBackend) {
+    if (disabledByBackend && !Strings.isNullOrEmpty(suppliedValue)) {
+      String msg = String.format("LDAP backend doesn't support: ldap.%s",
+          configOption);
+      log.error(msg);
+      throw new IllegalArgumentException(msg);
+    }
+  }
+
   @Override
   public boolean allowsEdit(final Account.FieldName field) {
     return !readOnlyAccountFields.contains(field);
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 6723db8..f2d40c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -16,25 +16,29 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -43,7 +47,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
+import java.util.Collections;
 
 @Singleton
 public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
@@ -54,100 +58,118 @@
   private final AbandonedSender.Factory abandonedSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
-  private final ChangeIndexer indexer;
-  private final ChangeUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
   Abandon(ChangeHooks hooks,
       AbandonedSender.Factory abandonedSenderFactory,
       Provider<ReviewDb> dbProvider,
       ChangeJson.Factory json,
-      ChangeIndexer indexer,
-      ChangeUpdate.Factory updateFactory,
-      ChangeMessagesUtil cmUtil) {
+      ChangeMessagesUtil cmUtil,
+      BatchUpdate.Factory batchUpdateFactory) {
     this.hooks = hooks;
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
-    this.indexer = indexer;
-    this.updateFactory = updateFactory;
     this.cmUtil = cmUtil;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   @Override
-  public ChangeInfo apply(ChangeResource req, AbandonInput input)
-      throws AuthException, ResourceConflictException, OrmException,
-      IOException {
+  public ChangeInfo apply(ChangeResource req,
+      final AbandonInput input)
+      throws RestApiException, UpdateException, OrmException {
     ChangeControl control = req.getControl();
-    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
-    Change change = req.getChange();
+    IdentifiedUser caller = control.getUser().asIdentifiedUser();
     if (!control.canAbandon()) {
       throw new AuthException("abandon not permitted");
-    } else if (!change.getStatus().isOpen()) {
-      throw new ResourceConflictException("change is " + status(change));
-    } else if (change.getStatus() == Change.Status.DRAFT) {
-      throw new ResourceConflictException("draft changes cannot be abandoned");
     }
-    change = abandon(control, input.message, caller.getAccount());
+    Change change = abandon(control, input.message, caller.getAccount());
     return json.create(ChangeJson.NO_OPTIONS).format(change);
   }
 
   public Change abandon(ChangeControl control,
-      String msgTxt, Account acc) throws ResourceConflictException,
-      OrmException, IOException {
-    Change change;
-    ChangeMessage message;
-    ChangeUpdate update;
-    Change.Id changeId = control.getChange().getId();
-    ReviewDb db = dbProvider.get();
-    db.changes().beginTransaction(changeId);
-    try {
-      change = db.changes().atomicUpdate(
-        changeId,
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus().isOpen()) {
-              change.setStatus(Change.Status.ABANDONED);
-              ChangeUtil.updated(change);
-              return change;
-            }
-            return null;
-          }
-        });
-      if (change == null) {
-        throw new ResourceConflictException("change is "
-            + status(db.changes().get(changeId)));
+      final String msgTxt, final Account account)
+      throws RestApiException, UpdateException {
+    Op op = new Op(msgTxt, account);
+    Change c = control.getChange();
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        c.getProject(), control.getUser(), TimeUtil.nowTs())) {
+      u.addOp(c.getId(), op).execute();
+    }
+    return op.change;
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final Account account;
+    private final String msgTxt;
+
+    private Change change;
+    private PatchSet patchSet;
+    private ChangeMessage message;
+
+    private Op(String msgTxt, Account account) {
+      this.account = account;
+      this.msgTxt = msgTxt;
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException,
+        ResourceConflictException {
+      change = ctx.getChange();
+      if (change == null || !change.getStatus().isOpen()) {
+        throw new ResourceConflictException("change is " + status(change));
+      } else if (change.getStatus() == Change.Status.DRAFT) {
+        throw new ResourceConflictException(
+            "draft changes cannot be abandoned");
+      }
+      patchSet = ctx.getDb().patchSets().get(change.currentPatchSetId());
+      change.setStatus(Change.Status.ABANDONED);
+      change.setLastUpdatedOn(ctx.getWhen());
+      ctx.getDb().changes().update(Collections.singleton(change));
+
+      message = newMessage(ctx.getDb());
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), message);
+    }
+
+    private ChangeMessage newMessage(ReviewDb db) throws OrmException {
+      StringBuilder msg = new StringBuilder();
+      msg.append("Abandoned");
+      if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) {
+        msg.append("\n\n");
+        msg.append(msgTxt.trim());
       }
 
-      //TODO(yyonas): atomic update was not propagated
-      update = updateFactory.create(control, change.getLastUpdatedOn());
-      message = newMessage(msgTxt, acc != null ? acc.getId() : null, change);
-      cmUtil.addChangeMessage(db, update, message);
-      db.commit();
-    } finally {
-      db.rollback();
+      ChangeMessage message = new ChangeMessage(
+          new ChangeMessage.Key(
+              change.getId(),
+              ChangeUtil.messageUUID(db)),
+          account != null ? account.getId() : null,
+          change.getLastUpdatedOn(),
+          change.currentPatchSetId());
+      message.setMessage(msg.toString());
+      return message;
     }
-    update.commit();
 
-    indexer.index(db, change);
-    try {
-      ReplyToChangeSender cm = abandonedSenderFactory.create(change.getId());
-      if (acc != null) {
-        cm.setFrom(acc.getId());
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      try {
+        ReplyToChangeSender cm = abandonedSenderFactory.create(change.getId());
+        if (account != null) {
+          cm.setFrom(account.getId());
+        }
+        cm.setChangeMessage(message);
+        cm.send();
+      } catch (Exception e) {
+        log.error("Cannot email update for change " + change.getId(), e);
       }
-      cm.setChangeMessage(message);
-      cm.send();
-    } catch (Exception e) {
-      log.error("Cannot email update for change " + change.getChangeId(), e);
+      hooks.doChangeAbandonedHook(change,
+          account,
+          patchSet,
+          Strings.emptyToNull(msgTxt),
+          ctx.getDb());
     }
-    hooks.doChangeAbandonedHook(change,
-        acc,
-        db.patchSets().get(change.currentPatchSetId()),
-        Strings.emptyToNull(msgTxt),
-        db);
-    return change;
   }
 
   @Override
@@ -160,26 +182,6 @@
           && resource.getControl().canAbandon());
   }
 
-  private ChangeMessage newMessage(String msgTxt, Account.Id accId,
-      Change change) throws OrmException {
-    StringBuilder msg = new StringBuilder();
-    msg.append("Abandoned");
-    if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) {
-      msg.append("\n\n");
-      msg.append(msgTxt.trim());
-    }
-
-    ChangeMessage message = new ChangeMessage(
-        new ChangeMessage.Key(
-            change.getId(),
-            ChangeUtil.messageUUID(dbProvider.get())),
-        accId,
-        change.getLastUpdatedOn(),
-        change.currentPatchSetId());
-    message.setMessage(msg.toString());
-    return message;
-  }
-
   private static String status(Change change) {
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index fd20868..d8574f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -62,11 +62,11 @@
 
   private Map<String, ActionInfo> toActionMap(ChangeControl ctl) {
     Map<String, ActionInfo> out = new LinkedHashMap<>();
-    if (!ctl.getCurrentUser().isIdentifiedUser()) {
+    if (!ctl.getUser().isIdentifiedUser()) {
       return out;
     }
 
-    Provider<CurrentUser> userProvider = Providers.of(ctl.getCurrentUser());
+    Provider<CurrentUser> userProvider = Providers.of(ctl.getUser());
     for (UiAction.Description d : UiActions.from(
         changeViews,
         new ChangeResource(ctl),
@@ -89,9 +89,9 @@
 
   private Map<String, ActionInfo> toActionMap(RevisionResource rsrc) {
     Map<String, ActionInfo> out = new LinkedHashMap<>();
-    if (rsrc.getControl().getCurrentUser().isIdentifiedUser()) {
+    if (rsrc.getControl().getUser().isIdentifiedUser()) {
       Provider<CurrentUser> userProvider = Providers.of(
-          rsrc.getControl().getCurrentUser());
+          rsrc.getControl().getUser());
       for (UiAction.Description d : UiActions.from(
           revisions, rsrc, userProvider)) {
         out.put(d.getId(), new ActionInfo(d));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 73f9cab..cb3729b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -272,8 +272,7 @@
 
     @Override
     public Response<EditInfo> apply(ChangeResource rsrc) throws AuthException,
-        IOException, InvalidChangeOperationException,
-        ResourceNotFoundException, OrmException {
+        IOException, ResourceNotFoundException, OrmException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
       if (!edit.isPresent()) {
         return Response.none();
@@ -382,7 +381,7 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
-        throws AuthException, ResourceConflictException, IOException {
+        throws AuthException, ResourceConflictException {
       String path = rsrc.getPath();
       if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
         throw new ResourceConflictException("Invalid path: " + path);
@@ -442,7 +441,7 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc)
-        throws ResourceNotFoundException, IOException {
+        throws IOException {
       try {
         return Response.ok(fileContentUtil.getContent(
               rsrc.getControl().getProjectControl().getProjectState(),
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 a9153d6..344eda1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -31,25 +31,33 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 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.IdentifiedUser;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -58,93 +66,104 @@
 import java.util.Map;
 import java.util.Set;
 
-public class ChangeInserter {
+public class ChangeInserter extends BatchUpdate.InsertChangeOp {
   public static interface Factory {
-    ChangeInserter create(ProjectControl ctl, Change c, RevCommit rc);
+    ChangeInserter create(RefControl ctl, Change c, RevCommit rc);
   }
 
   private static final Logger log =
       LoggerFactory.getLogger(ChangeInserter.class);
 
-  private final Provider<ReviewDb> dbProvider;
-  private final ChangeUpdate.Factory updateFactory;
-  private final GitReferenceUpdated gitRefUpdated;
+  private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
-  private final ChangeIndexer indexer;
   private final CreateChangeSender.Factory createChangeSenderFactory;
-  private final HashtagsUtil hashtagsUtil;
-  private final AccountCache accountCache;
   private final WorkQueue workQueue;
+  private final CommitValidators.Factory commitValidatorsFactory;
 
-  private final ProjectControl projectControl;
+  private final RefControl refControl;
+  private final IdentifiedUser user;
   private final Change change;
   private final PatchSet patchSet;
   private final RevCommit commit;
-  private final PatchSetInfo patchSetInfo;
 
-  private ChangeMessage changeMessage;
+  // Fields exposed as setters.
+  private String message;
+  private CommitValidators.Policy validatePolicy =
+      CommitValidators.Policy.GERRIT;
   private Set<Account.Id> reviewers;
   private Set<Account.Id> extraCC;
   private Map<String, Short> approvals;
-  private Set<String> hashtags;
   private RequestScopePropagator requestScopePropagator;
   private boolean runHooks;
   private boolean sendMail;
+  private boolean updateRef;
+
+  // Fields set during the insertion process.
+  private ChangeMessage changeMessage;
+  private PatchSetInfo patchSetInfo;
 
   @Inject
-  ChangeInserter(Provider<ReviewDb> dbProvider,
-      ChangeUpdate.Factory updateFactory,
-      PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated,
+  ChangeInserter(PatchSetInfoFactory patchSetInfoFactory,
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      ChangeIndexer indexer,
       CreateChangeSender.Factory createChangeSenderFactory,
-      HashtagsUtil hashtagsUtil,
-      AccountCache accountCache,
       WorkQueue workQueue,
-      @Assisted ProjectControl projectControl,
+      CommitValidators.Factory commitValidatorsFactory,
+      @Assisted RefControl refControl,
       @Assisted Change change,
       @Assisted RevCommit commit) {
-    this.dbProvider = dbProvider;
-    this.updateFactory = updateFactory;
-    this.gitRefUpdated = gitRefUpdated;
+    String projectName = refControl.getProjectControl().getProject().getName();
+    String refName = refControl.getRefName();
+    checkArgument(projectName.equals(change.getProject().get())
+          && refName.equals(change.getDest().get()),
+        "RefControl for %s,%s does not match change destination %s",
+        projectName, refName, change.getDest());
+
+    this.patchSetInfoFactory = patchSetInfoFactory;
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.indexer = indexer;
     this.createChangeSenderFactory = createChangeSenderFactory;
-    this.hashtagsUtil = hashtagsUtil;
-    this.accountCache = accountCache;
     this.workQueue = workQueue;
-    this.projectControl = projectControl;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+
+    this.refControl = refControl;
     this.change = change;
     this.commit = commit;
     this.reviewers = Collections.emptySet();
     this.extraCC = Collections.emptySet();
     this.approvals = Collections.emptyMap();
-    this.hashtags = Collections.emptySet();
     this.runHooks = true;
     this.sendMail = true;
+    this.updateRef = true;
 
+    user = refControl.getUser().asIdentifiedUser();
     patchSet =
         new PatchSet(new PatchSet.Id(change.getId(), INITIAL_PATCH_SET_ID));
     patchSet.setCreatedOn(change.getCreatedOn());
     patchSet.setUploader(change.getOwner());
     patchSet.setRevision(new RevId(commit.name()));
-    patchSetInfo = patchSetInfoFactory.get(commit, patchSet.getId());
-    change.setCurrentPatchSet(patchSetInfo);
   }
 
+  @Override
   public Change getChange() {
     return change;
   }
 
-  public ChangeInserter setMessage(ChangeMessage changeMessage) {
-    this.changeMessage = changeMessage;
+  public IdentifiedUser getUser() {
+    return user;
+  }
+
+  public ChangeInserter setMessage(String message) {
+    this.message = message;
+    return this;
+  }
+
+  public ChangeInserter setValidatePolicy(CommitValidators.Policy validate) {
+    this.validatePolicy = checkNotNull(validate);
     return this;
   }
 
@@ -169,11 +188,6 @@
     return this;
   }
 
-  public ChangeInserter setHashtags(Set<String> hashtags) {
-    this.hashtags = hashtags;
-    return this;
-  }
-
   public ChangeInserter setRunHooks(boolean runHooks) {
     this.runHooks = runHooks;
     return this;
@@ -198,55 +212,62 @@
     return this;
   }
 
-  public PatchSetInfo getPatchSetInfo() {
-    return patchSetInfo;
+  public ChangeInserter setUpdateRef(boolean updateRef) {
+    this.updateRef = updateRef;
+    return this;
   }
 
-  public Change insert() throws OrmException, IOException {
-    ReviewDb db = dbProvider.get();
-    ChangeControl ctl = projectControl.controlFor(change);
-    ChangeUpdate update = updateFactory.create(
-        ctl,
-        change.getCreatedOn());
-    db.changes().beginTransaction(change.getId());
-    try {
-      ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
-      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();
-      approvalsUtil.addReviewers(db, update, labelTypes, change,
-          patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
-      approvalsUtil.addApprovals(db, update, labelTypes, patchSet, patchSetInfo,
-          ctl, approvals);
-      if (messageIsForChange()) {
-        cmUtil.addChangeMessage(db, update, changeMessage);
-      }
-      db.commit();
-    } finally {
-      db.rollback();
+  public ChangeMessage getChangeMessage() {
+    if (message == null) {
+      return null;
     }
+    checkState(changeMessage != null,
+        "getChangeMessage() only valid after inserting change");
+    return changeMessage;
+  }
 
-    update.commit();
-
-    if (hashtags != null && hashtags.size() > 0) {
-      try {
-        HashtagsInput input = new HashtagsInput();
-        input.add = hashtags;
-        hashtagsUtil.setHashtags(ctl, input, false, false);
-      } catch (ValidationException | AuthException e) {
-        log.error("Cannot add hashtags to change " + change.getId(), e);
-      }
+  @Override
+  public void updateRepo(RepoContext ctx)
+      throws InvalidChangeOperationException, IOException {
+    validate(ctx);
+    patchSetInfo = patchSetInfoFactory.get(
+        ctx.getRevWalk(), commit, patchSet.getId());
+    change.setCurrentPatchSet(patchSetInfo);
+    if (!updateRef) {
+      return;
     }
+    ctx.addRefUpdate(
+        new ReceiveCommand(ObjectId.zeroId(), commit, patchSet.getRefName()));
+  }
 
-    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
-    if (!messageIsForChange()) {
-      commitMessageNotForChange();
+  @Override
+  public void updateChange(ChangeContext ctx) throws OrmException, IOException {
+    ReviewDb db = ctx.getDb();
+    ChangeControl ctl = ctx.getChangeControl();
+    ChangeUpdate update = ctx.getChangeUpdate();
+    ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+    if (patchSet.getGroups() == null) {
+      patchSet.setGroups(GroupCollector.getDefaultGroups(patchSet));
     }
-    f.checkedGet();
+    db.patchSets().insert(Collections.singleton(patchSet));
+    db.changes().insert(Collections.singleton(change));
+    LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
+    approvalsUtil.addReviewers(db, update, labelTypes, change,
+        patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
+    approvalsUtil.addApprovals(db, update, labelTypes, patchSet,
+        ctx.getChangeControl(), approvals);
+    if (message != null) {
+      changeMessage =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(db)), user.getAccountId(),
+              patchSet.getCreatedOn(), patchSet.getId());
+      changeMessage.setMessage(message);
+      cmUtil.addChangeMessage(db, update, changeMessage);
+    }
+  }
 
+  @Override
+  public void postUpdate(Context ctx) throws OrmException {
     if (sendMail) {
       Runnable sender = new Runnable() {
         @Override
@@ -276,43 +297,50 @@
       }
     }
 
-    gitRefUpdated.fire(change.getProject(), patchSet.getRefName(),
-        ObjectId.zeroId(), commit);
-
     if (runHooks) {
+      ReviewDb db = ctx.getDb();
       hooks.doPatchsetCreatedHook(change, patchSet, db);
-      if (hashtags != null && hashtags.size() > 0) {
-        hooks.doHashtagsChangedHook(change,
-            accountCache.get(change.getOwner()).getAccount(),
-            hashtags, null, hashtags, db);
+      if (approvals != null && !approvals.isEmpty()) {
+        hooks.doCommentAddedHook(
+            change, user.getAccount(), patchSet, null, approvals, db);
       }
     }
-
-    return change;
   }
 
-  private void commitMessageNotForChange() throws OrmException,
-      IOException {
-    ReviewDb db = dbProvider.get();
-    if (changeMessage != null) {
-      Change otherChange =
-          db.changes().get(changeMessage.getPatchSetId().getParentKey());
-      ChangeUtil.bumpRowVersionNotLastUpdatedOn(
-          changeMessage.getKey().getParentKey(), db);
-      ChangeControl otherControl = projectControl.controlFor(otherChange);
-      ChangeUpdate updateForOtherChange =
-          updateFactory.create(otherControl, change.getLastUpdatedOn());
-      cmUtil.addChangeMessage(db, updateForOtherChange, changeMessage);
-      updateForOtherChange.commit();
+  private void validate(RepoContext ctx)
+      throws IOException, InvalidChangeOperationException {
+    if (validatePolicy == CommitValidators.Policy.NONE) {
+      return;
     }
-  }
+    CommitValidators cv = commitValidatorsFactory.create(
+        refControl, new NoSshInfo(), ctx.getRepository());
 
-  private boolean messageIsForChange() {
-    if (changeMessage == null) {
-      return false;
+    String refName = patchSet.getId().toRefName();
+    CommitReceivedEvent event = new CommitReceivedEvent(
+        new ReceiveCommand(
+            ObjectId.zeroId(),
+            commit.getId(),
+            refName),
+        refControl.getProjectControl().getProject(),
+        change.getDest().get(),
+        commit,
+        user);
+
+    try {
+      switch (validatePolicy) {
+      case RECEIVE_COMMITS:
+        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(
+            ctx.getRepository(), ctx.getRevWalk());
+        cv.validateForReceiveCommits(event, rejectCommits);
+        break;
+      case GERRIT:
+        cv.validateForGerritCommits(event);
+        break;
+      case NONE:
+        break;
+      }
+    } catch (CommitValidationException e) {
+      throw new InvalidChangeOperationException(e.getMessage());
     }
-    Change.Id id = change.getId();
-    Change.Id msgId = changeMessage.getKey().getParentKey();
-    return msgId.equals(id);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index acdf004..7d126e3 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
@@ -29,6 +29,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
 import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
 import static com.google.gerrit.server.CommonConverters.toGitPerson;
@@ -67,6 +68,7 @@
 import com.google.gerrit.extensions.common.FetchInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.config.DownloadCommand;
@@ -84,9 +86,11 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.api.accounts.GpgApiAdapter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.git.MergeUtil;
@@ -149,6 +153,7 @@
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ConsistencyChecker> checkerProvider;
   private final ActionJson actionJson;
+  private final GpgApiAdapter gpgApi;
 
   private AccountLoader accountLoader;
   private FixInput fix;
@@ -173,6 +178,7 @@
       ChangeMessagesUtil cmUtil,
       Provider<ConsistencyChecker> checkerProvider,
       ActionJson actionJson,
+      GpgApiAdapter gpgApi,
       @Assisted Set<ListChangesOption> options) {
     this.db = db;
     this.labelNormalizer = ln;
@@ -192,6 +198,7 @@
     this.cmUtil = cmUtil;
     this.checkerProvider = checkerProvider;
     this.actionJson = actionJson;
+    this.gpgApi = gpgApi;
     this.options = options.isEmpty()
         ? EnumSet.noneOf(ListChangesOption.class)
         : EnumSet.copyOf(options);
@@ -254,8 +261,8 @@
       } else {
         return toChangeInfo(cd, limitToPsId);
       }
-    } catch (PatchListNotAvailableException | OrmException | IOException
-        | RuntimeException e) {
+    } catch (PatchListNotAvailableException | GpgException | OrmException
+        | IOException | RuntimeException e) {
       if (!has(CHECK)) {
         Throwables.propagateIfPossible(e, OrmException.class);
         throw new OrmException(e);
@@ -272,13 +279,40 @@
   public List<List<ChangeInfo>> formatQueryResults(List<QueryResult> in)
       throws OrmException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    Iterable<ChangeData> all = FluentIterable.from(in)
+    ensureLoaded(FluentIterable.from(in)
         .transformAndConcat(new Function<QueryResult, List<ChangeData>>() {
           @Override
           public List<ChangeData> apply(QueryResult in) {
             return in.changes();
           }
-        });
+        }));
+
+    List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
+    Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
+    for (QueryResult r : in) {
+      List<ChangeInfo> infos = toChangeInfo(out, r.changes());
+      if (!infos.isEmpty() && r.moreChanges()) {
+        infos.get(infos.size() - 1)._moreChanges = true;
+      }
+      res.add(infos);
+    }
+    accountLoader.fill();
+    return res;
+  }
+
+  public List<ChangeInfo> formatChangeDatas(Collection<ChangeData> in)
+      throws OrmException {
+    accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+    ensureLoaded(in);
+    List<ChangeInfo> out = new ArrayList<>(in.size());
+    for (ChangeData cd : in) {
+      out.add(format(cd));
+    }
+    accountLoader.fill();
+    return out;
+  }
+
+  private void ensureLoaded(Iterable<ChangeData> all) throws OrmException {
     ChangeData.ensureChangeLoaded(all);
     if (has(ALL_REVISIONS)) {
       ChangeData.ensureAllPatchSetsLoaded(all);
@@ -289,18 +323,6 @@
       ChangeData.ensureReviewedByLoadedForOpenChanges(all);
     }
     ChangeData.ensureCurrentApprovalsLoaded(all);
-
-    List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
-    Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
-    for (QueryResult r : in) {
-      List<ChangeInfo> infos = toChangeInfo(out, r.changes());
-      if (r.moreChanges()) {
-        infos.get(infos.size() - 1)._moreChanges = true;
-      }
-      res.add(infos);
-    }
-    accountLoader.fill();
-    return res;
   }
 
   private boolean has(ListChangesOption option) {
@@ -315,8 +337,8 @@
       if (i == null) {
         try {
           i = toChangeInfo(cd, Optional.<PatchSet.Id> absent());
-        } catch (PatchListNotAvailableException | OrmException | IOException
-            | RuntimeException e) {
+        } catch (PatchListNotAvailableException | GpgException | OrmException
+            | IOException | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
           } else {
@@ -359,8 +381,8 @@
   }
 
   private ChangeInfo toChangeInfo(ChangeData cd,
-      Optional<PatchSet.Id> limitToPsId)
-      throws PatchListNotAvailableException, OrmException, IOException {
+      Optional<PatchSet.Id> limitToPsId) throws PatchListNotAvailableException,
+      GpgException, OrmException, IOException {
     ChangeInfo out = new ChangeInfo();
 
     if (has(CHECK)) {
@@ -402,7 +424,7 @@
         ? true
         : null;
     if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
-      Account.Id accountId = ((IdentifiedUser) user).getAccountId();
+      Account.Id accountId = user.getAccountId();
       out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
     }
 
@@ -816,8 +838,8 @@
   }
 
   private Map<String, RevisionInfo> revisions(ChangeControl ctl,
-      Map<PatchSet.Id, PatchSet> map)
-      throws PatchListNotAvailableException, OrmException, IOException {
+      Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
+      GpgException, OrmException, IOException {
     Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
     for (PatchSet in : map.values()) {
       if ((has(ALL_REVISIONS)
@@ -858,7 +880,8 @@
   }
 
   private RevisionInfo toRevisionInfo(ChangeControl ctl, PatchSet in)
-      throws PatchListNotAvailableException, OrmException, IOException {
+      throws PatchListNotAvailableException, GpgException, OrmException,
+      IOException {
     Change c = ctl.getChange();
     RevisionInfo out = new RevisionInfo();
     out.isCurrent = in.getId().equals(c.currentPatchSetId());
@@ -903,6 +926,16 @@
           new RevisionResource(new ChangeResource(ctl), in));
     }
 
+    if (has(PUSH_CERTIFICATES)) {
+      if (in.getPushCertificate() != null) {
+        out.pushCertificate = gpgApi.checkPushCertificate(
+            in.getPushCertificate(),
+            userFactory.create(db, in.getUploader()));
+      } else {
+        out.pushCertificate = new PushCertificateInfo();
+      }
+    }
+
     return out;
   }
 
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 84658b2..03d189f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -20,9 +20,9 @@
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestResource.HasETag;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
@@ -62,9 +62,13 @@
   public void prepareETag(Hasher h, CurrentUser user) {
     h.putLong(getChange().getLastUpdatedOn().getTime())
       .putInt(getChange().getRowVersion())
-      .putInt(user.isIdentifiedUser()
-          ? ((IdentifiedUser) user).getAccountId().get()
-          : 0);
+      .putInt(user.isIdentifiedUser() ? user.getAccountId().get() : 0);
+
+    if (user.isIdentifiedUser()) {
+      for (AccountGroup.UUID uuid : user.getEffectiveGroups().getKnownGroups()) {
+        h.putBytes(uuid.get().getBytes());
+      }
+    }
 
     byte[] buf = new byte[20];
     ObjectId noteId;
@@ -84,7 +88,7 @@
 
   @Override
   public String getETag() {
-    CurrentUser user = control.getCurrentUser();
+    CurrentUser user = control.getUser();
     Hasher h = Hashing.md5().newHasher()
         .putBoolean(user.getStarredChanges().contains(getChange().getId()));
     prepareETag(h, user);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index f4b2f4e..d7fe43b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -47,7 +47,7 @@
     ChangeControl ctl = rsrc.getControl();
     if (!ctl.isOwner()
         && !ctl.getProjectControl().isOwner()
-        && !ctl.getCurrentUser().getCapabilities().canMaintainServer()) {
+        && !ctl.getUser().getCapabilities().canMaintainServer()) {
       throw new AuthException("Cannot fix change");
     }
     return Response.withMustRevalidate(newChangeJson().fix(input).format(rsrc));
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 ad5940e..b0f14af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -14,19 +14,19 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -56,8 +56,7 @@
 
   @Override
   public ChangeInfo apply(RevisionResource revision, CherryPickInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      ResourceNotFoundException, OrmException, IOException, EmailException {
+      throws OrmException, IOException, UpdateException, RestApiException {
     final ChangeControl control = revision.getControl();
 
     if (input.message == null || input.message.trim().isEmpty()) {
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 34a7070..1045cff 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
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -26,16 +27,17 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
@@ -45,7 +47,6 @@
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -58,11 +59,8 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 import java.io.IOException;
@@ -76,37 +74,37 @@
   private final Provider<InternalChangeQuery> queryProvider;
   private final GitRepositoryManager gitManager;
   private final TimeZone serverTimeZone;
-  private final Provider<CurrentUser> currentUser;
-  private final CommitValidators.Factory commitValidatorsFactory;
+  private final Provider<IdentifiedUser> user;
   private final ChangeInserter.Factory changeInserterFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final ChangeUpdate.Factory updateFactory;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
   CherryPickChange(Provider<ReviewDb> db,
       Provider<InternalChangeQuery> queryProvider,
       @GerritPersonIdent PersonIdent myIdent,
       GitRepositoryManager gitManager,
-      Provider<CurrentUser> currentUser,
-      CommitValidators.Factory commitValidatorsFactory,
+      Provider<IdentifiedUser> user,
       ChangeInserter.Factory changeInserterFactory,
       PatchSetInserter.Factory patchSetInserterFactory,
       MergeUtil.Factory mergeUtilFactory,
       ChangeMessagesUtil changeMessagesUtil,
-      ChangeUpdate.Factory updateFactory) {
+      ChangeUpdate.Factory updateFactory,
+      BatchUpdate.Factory batchUpdateFactory) {
     this.db = db;
     this.queryProvider = queryProvider;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
-    this.currentUser = currentUser;
-    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.user = user;
     this.changeInserterFactory = changeInserterFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeMessagesUtil = changeMessagesUtil;
     this.updateFactory = updateFactory;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   public Change.Id cherryPick(Change change, PatchSet patch,
@@ -114,7 +112,8 @@
       final RefControl refControl) throws NoSuchChangeException,
       OrmException, MissingObjectException,
       IncorrectObjectTypeException, IOException,
-      InvalidChangeOperationException, MergeException {
+      InvalidChangeOperationException, MergeException, UpdateException,
+      RestApiException {
 
     if (Strings.isNullOrEmpty(ref)) {
       throw new InvalidChangeOperationException(
@@ -123,18 +122,18 @@
 
     Project.NameKey project = change.getProject();
     String destinationBranch = RefNames.shortName(ref);
-    IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
+    IdentifiedUser identifiedUser = user.get();
     try (Repository git = gitManager.openRepository(project);
-        RevWalk revWalk = new RevWalk(git)) {
+        CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(git)) {
       Ref destRef = git.getRefDatabase().exactRef(ref);
       if (destRef == null) {
         throw new InvalidChangeOperationException(String.format(
             "Branch %s does not exist.", destinationBranch));
       }
 
-      final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
+      CodeReviewCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
 
-      RevCommit commitToCherryPick =
+      CodeReviewCommit commitToCherryPick =
           revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
 
       PersonIdent committerIdent =
@@ -148,131 +147,115 @@
       String commitMessage =
           ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
 
-      RevCommit cherryPickCommit;
+      CodeReviewCommit cherryPickCommit;
       try (ObjectInserter oi = git.newObjectInserter()) {
         ProjectState projectState = refControl.getProjectControl().getProjectState();
         cherryPickCommit =
             mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
                 commitToCherryPick, committerIdent, commitMessage, revWalk);
+
+        Change.Key changeKey;
+        final List<String> idList = cherryPickCommit.getFooterLines(
+            FooterConstants.CHANGE_ID);
+        if (!idList.isEmpty()) {
+          final String idStr = idList.get(idList.size() - 1).trim();
+          changeKey = new Change.Key(idStr);
+        } else {
+          changeKey = new Change.Key("I" + computedChangeId.name());
+        }
+
+        Branch.NameKey newDest =
+            new Branch.NameKey(change.getProject(), destRef.getName());
+        List<ChangeData> destChanges = queryProvider.get()
+            .setLimit(2)
+            .byBranchKey(newDest, changeKey);
+        if (destChanges.size() > 1) {
+          throw new InvalidChangeOperationException("Several changes with key "
+              + changeKey + " reside on the same branch. "
+              + "Cannot create a new patch set.");
+        } else if (destChanges.size() == 1) {
+          // The change key exists on the destination branch. The cherry pick
+          // will be added as a new patch set.
+          return insertPatchSet(git, revWalk, oi, destChanges.get(0).change(),
+              cherryPickCommit, refControl, identifiedUser);
+        } else {
+          // Change key not found on destination branch. We can create a new
+          // change.
+          String newTopic = null;
+          if (!Strings.isNullOrEmpty(change.getTopic())) {
+            newTopic = change.getTopic() + "-" + newDest.getShortName();
+          }
+          Change newChange = createNewChange(git, revWalk, oi, changeKey,
+              project, destRef, cherryPickCommit, refControl, identifiedUser,
+              newTopic, change.getDest());
+
+          addMessageToSourceChange(change, patch.getId(), destinationBranch,
+              cherryPickCommit, identifiedUser, refControl);
+
+          return newChange.getId();
+        }
       } catch (MergeIdenticalTreeException | MergeConflictException e) {
         throw new MergeException("Cherry pick failed: " + e.getMessage());
       }
-
-      Change.Key changeKey;
-      final List<String> idList = cherryPickCommit.getFooterLines(
-          FooterConstants.CHANGE_ID);
-      if (!idList.isEmpty()) {
-        final String idStr = idList.get(idList.size() - 1).trim();
-        changeKey = new Change.Key(idStr);
-      } else {
-        changeKey = new Change.Key("I" + computedChangeId.name());
-      }
-
-      Branch.NameKey newDest =
-          new Branch.NameKey(change.getProject(), destRef.getName());
-      List<ChangeData> destChanges = queryProvider.get()
-          .setLimit(2)
-          .byBranchKey(newDest, changeKey);
-      if (destChanges.size() > 1) {
-        throw new InvalidChangeOperationException("Several changes with key "
-            + changeKey + " reside on the same branch. "
-            + "Cannot create a new patch set.");
-      } else if (destChanges.size() == 1) {
-        // The change key exists on the destination branch. The cherry pick
-        // will be added as a new patch set.
-        return insertPatchSet(git, revWalk, destChanges.get(0).change(),
-            cherryPickCommit, refControl, identifiedUser);
-      } else {
-        // Change key not found on destination branch. We can create a new
-        // change.
-        String newTopic = null;
-        if (!Strings.isNullOrEmpty(change.getTopic())) {
-          newTopic = change.getTopic() + "-" + newDest.getShortName();
-        }
-        Change newChange = createNewChange(git, revWalk, changeKey, project,
-            destRef, cherryPickCommit, refControl,
-            identifiedUser, newTopic);
-
-        addMessageToSourceChange(change, patch.getId(), destinationBranch,
-            cherryPickCommit, identifiedUser, refControl);
-
-        addMessageToDestinationChange(newChange, change.getDest().getShortName(),
-            identifiedUser, refControl);
-
-        return newChange.getId();
-      }
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(change.getId(), e);
     }
   }
 
-  private Change.Id insertPatchSet(Repository git, RevWalk revWalk, Change change,
-      RevCommit cherryPickCommit, RefControl refControl,
-      IdentifiedUser identifiedUser)
-      throws InvalidChangeOperationException, IOException, OrmException,
-      NoSuchChangeException {
-    final ChangeControl changeControl =
-        refControl.getProjectControl().controlFor(change);
-    final PatchSetInserter inserter = patchSetInserterFactory
-        .create(git, revWalk, changeControl, cherryPickCommit);
-    final PatchSet.Id newPatchSetId = inserter.getPatchSetId();
+  private Change.Id insertPatchSet(Repository git, RevWalk revWalk,
+      ObjectInserter oi, Change change, CodeReviewCommit cherryPickCommit,
+      RefControl refControl, IdentifiedUser identifiedUser)
+      throws IOException, OrmException, UpdateException, RestApiException {
+    PatchSet.Id psId =
+        ChangeUtil.nextPatchSetId(git, change.currentPatchSetId());
+    PatchSetInserter inserter = patchSetInserterFactory
+        .create(refControl, psId, cherryPickCommit);
+    PatchSet.Id newPatchSetId = inserter.getPatchSetId();
     PatchSet current = db.get().patchSets().get(change.currentPatchSetId());
-    inserter
-      .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
-      .setDraft(current.isDraft())
-      .setUploader(identifiedUser.getAccountId())
-      .setSendMail(false)
-      .insert();
+
+    try (BatchUpdate bu = batchUpdateFactory.create(
+        db.get(), change.getDest().getParentKey(), identifiedUser,
+        TimeUtil.nowTs())) {
+      bu.setRepository(git, revWalk, oi);
+      bu.addOp(change.getId(), inserter
+          .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
+          .setDraft(current.isDraft())
+          .setUploader(identifiedUser.getAccountId())
+          .setSendMail(false));
+      bu.execute();
+    }
     return change.getId();
   }
 
   private Change createNewChange(Repository git, RevWalk revWalk,
-      Change.Key changeKey, Project.NameKey project,
-      Ref destRef, RevCommit cherryPickCommit, RefControl refControl,
-      IdentifiedUser identifiedUser, String topic)
-      throws OrmException, InvalidChangeOperationException, IOException {
+      ObjectInserter oi, Change.Key changeKey, Project.NameKey project,
+      Ref destRef, CodeReviewCommit cherryPickCommit, RefControl refControl,
+      IdentifiedUser identifiedUser, String topic, Branch.NameKey sourceBranch)
+      throws RestApiException, UpdateException, OrmException {
     Change change =
         new Change(changeKey, new Change.Id(db.get().nextChangeId()),
             identifiedUser.getAccountId(), new Branch.NameKey(project,
                 destRef.getName()), TimeUtil.nowTs());
     change.setTopic(topic);
-    ChangeInserter ins =
-        changeInserterFactory.create(refControl.getProjectControl(), change,
-            cherryPickCommit);
-    PatchSet newPatchSet = ins.getPatchSet();
+    ChangeInserter ins = changeInserterFactory.create(
+          refControl, change, cherryPickCommit)
+        .setValidatePolicy(CommitValidators.Policy.GERRIT);
 
-    CommitValidators commitValidators =
-        commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
-    CommitReceivedEvent commitReceivedEvent =
-        new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
-            cherryPickCommit.getId(), newPatchSet.getRefName()), refControl
-            .getProjectControl().getProject(), refControl.getRefName(),
-            cherryPickCommit, identifiedUser);
-
-    try {
-      commitValidators.validateForGerritCommits(commitReceivedEvent);
-    } catch (CommitValidationException e) {
-      throw new InvalidChangeOperationException(e.getMessage());
+    ins.setMessage(
+        messageForDestinationChange(ins.getPatchSet().getId(), sourceBranch));
+    try (BatchUpdate bu = batchUpdateFactory.create(
+        db.get(), change.getProject(), identifiedUser, TimeUtil.nowTs())) {
+      bu.setRepository(git, revWalk, oi);
+      bu.insertChange(ins);
+      bu.execute();
     }
-
-    final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(cherryPickCommit);
-    ru.disableRefLog();
-    if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-      throw new IOException(String.format(
-          "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
-          change.getDest().getParentKey().get(), ru.getResult()));
-    }
-
-    ins.insert();
-
-    return change;
+    return ins.getChange();
   }
 
   private void addMessageToSourceChange(Change change, PatchSet.Id patchSetId,
-      String destinationBranch, RevCommit cherryPickCommit,
-      IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
+      String destinationBranch, CodeReviewCommit cherryPickCommit,
+      IdentifiedUser identifiedUser, RefControl refControl)
+          throws OrmException, IOException {
     ChangeMessage changeMessage = new ChangeMessage(
         new ChangeMessage.Key(
             patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
@@ -288,28 +271,18 @@
     changeMessage.setMessage(sb.toString());
 
     ChangeControl ctl = refControl.getProjectControl().controlFor(change);
-    ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
+    ChangeUpdate update = updateFactory.create(ctl, TimeUtil.nowTs());
     changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+    update.commit();
   }
 
-  private void addMessageToDestinationChange(Change change, String sourceBranch,
-      IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
-    PatchSet.Id patchSetId =
-        db.get().patchSets().get(change.currentPatchSetId()).getId();
-    ChangeMessage changeMessage = new ChangeMessage(
-        new ChangeMessage.Key(
-            patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
-            identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
-
-    StringBuilder sb = new StringBuilder("Patch Set ")
+  private String messageForDestinationChange(PatchSet.Id patchSetId,
+      Branch.NameKey sourceBranch) {
+    return new StringBuilder("Patch Set ")
       .append(patchSetId.get())
       .append(": Cherry Picked from branch ")
-      .append(sourceBranch)
-      .append(".");
-    changeMessage.setMessage(sb.toString());
-
-    ChangeControl ctl = refControl.getProjectControl().controlFor(change);
-    ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
-    changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+      .append(sourceBranch.getShortName())
+      .append(".")
+      .toString();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 7240f44..8a0a47e 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
@@ -28,9 +28,11 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.ProblemInfo.Status;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -39,14 +41,15 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -57,6 +60,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -106,9 +110,10 @@
   private final GitRepositoryManager repoManager;
   private final Provider<CurrentUser> user;
   private final Provider<PersonIdent> serverIdent;
-  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ProjectControl.GenericFactory projectControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final BatchUpdate.Factory updateFactory;
 
   private FixInput fix;
   private Change change;
@@ -127,16 +132,18 @@
       GitRepositoryManager repoManager,
       Provider<CurrentUser> user,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
-      ChangeControl.GenericFactory changeControlFactory,
+      ProjectControl.GenericFactory projectControlFactory,
       PatchSetInfoFactory patchSetInfoFactory,
-      PatchSetInserter.Factory patchSetInserterFactory) {
+      PatchSetInserter.Factory patchSetInserterFactory,
+      BatchUpdate.Factory updateFactory) {
     this.db = db;
     this.repoManager = repoManager;
     this.user = user;
     this.serverIdent = serverIdent;
-    this.changeControlFactory = changeControlFactory;
+    this.projectControlFactory = projectControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
+    this.updateFactory = updateFactory;
     reset();
   }
 
@@ -343,17 +350,17 @@
             + " is merged");
         return;
       }
-      checkMergedBitMatchesStatus(currPs, currPsCommit, merged);
+      checkMergedBitMatchesStatus(currPs.getId(), currPsCommit, merged);
     }
   }
 
-  private void checkMergedBitMatchesStatus(PatchSet ps, RevCommit commit,
+  private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit,
       boolean merged) {
     String refName = change.getDest().get();
     if (merged && change.getStatus() != Change.Status.MERGED) {
       ProblemInfo p = problem(String.format(
           "Patch set %d (%s) is merged into destination ref %s (%s), but change"
-          + " status is %s", ps.getId().get(), commit.name(),
+          + " status is %s", psId.get(), commit.name(),
           refName, tip.name(), change.getStatus()));
       if (fix != null) {
         fixMerged(p);
@@ -414,9 +421,9 @@
                   commit.name(), changeId, change.getKey().get()));
             return;
           }
-          PatchSet ps = insertPatchSet(commit);
-          if (ps != null) {
-            checkMergedBitMatchesStatus(ps, commit, true);
+          PatchSet.Id psId = insertPatchSet(commit);
+          if (psId != null) {
+            checkMergedBitMatchesStatus(psId, commit, true);
           }
           break;
 
@@ -445,7 +452,7 @@
     }
   }
 
-  private PatchSet insertPatchSet(RevCommit commit) {
+  private PatchSet.Id insertPatchSet(RevCommit commit) {
     ProblemInfo p =
         problem("No patch set found for merged commit " + commit.name());
     if (!user.get().isIdentifiedUser()) {
@@ -456,23 +463,33 @@
     }
 
     try {
-      ChangeControl ctl = changeControlFactory.controlFor(change, user.get());
+      RefControl ctl = projectControlFactory
+          .controlFor(change.getProject(), user.get())
+          .controlForRef(change.getDest());
+      PatchSet.Id psId =
+          ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
       PatchSetInserter inserter =
-          patchSetInserterFactory.create(repo, rw, ctl, commit);
-      change = inserter.setValidatePolicy(ValidatePolicy.NONE)
-          .setRunHooks(false)
-          .setSendMail(false)
-          .setAllowClosed(true)
-          .setUploader(((IdentifiedUser) user.get()).getAccountId())
-          // TODO: fix setMessage to work without init()
-          .setMessage(
-              "Patch set for merged commit inserted by consistency checker")
-          .insert();
+          patchSetInserterFactory.create(ctl, psId, commit);
+      try (BatchUpdate bu = updateFactory.create(
+            db.get(), change.getProject(), ctl.getUser(), TimeUtil.nowTs());
+          ObjectInserter oi = repo.newObjectInserter()) {
+        bu.setRepository(repo, rw, oi);
+        bu.addOp(change.getId(), inserter
+            .setValidatePolicy(CommitValidators.Policy.NONE)
+            .setRunHooks(false)
+            .setSendMail(false)
+            .setAllowClosed(true)
+            .setUploader(user.get().getAccountId())
+            .setMessage(
+                "Patch set for merged commit inserted by consistency checker"));
+        bu.execute();
+      }
+      change = inserter.getChange();
       p.status = Status.FIXED;
-      p.outcome = "Inserted as patch set " + change.currentPatchSetId().get();
-      return inserter.getPatchSet();
-    } catch (InvalidChangeOperationException | OrmException | IOException
-        | NoSuchChangeException e) {
+      p.outcome = "Inserted as patch set " + psId.get();
+      return psId;
+    } catch (IOException | NoSuchProjectException | UpdateException
+        | RestApiException e) {
       warn(e);
       p.status = Status.FIX_FAILED;
       p.outcome = "Error inserting new patch set";
@@ -591,7 +608,7 @@
   private PersonIdent newRefLogIdent() {
     CurrentUser u = user.get();
     if (u.isIdentifiedUser()) {
-      return ((IdentifiedUser) u).newRefLogIdent();
+      return u.asIdentifiedUser().newRefLogIdent();
     } else {
       return serverIdent.get();
     }
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 6c0ee3b..3768738 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -24,15 +24,13 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -42,15 +40,14 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.RefControl;
-import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -62,11 +59,9 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 import java.io.IOException;
@@ -82,43 +77,41 @@
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
   private final TimeZone serverTimeZone;
-  private final Provider<CurrentUser> userProvider;
+  private final Provider<CurrentUser> user;
   private final ProjectsCollection projectsCollection;
-  private final CommitValidators.Factory commitValidatorsFactory;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeJson.Factory jsonFactory;
   private final ChangeUtil changeUtil;
+  private final BatchUpdate.Factory updateFactory;
   private final boolean allowDrafts;
 
   @Inject
   CreateChange(Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
       @GerritPersonIdent PersonIdent myIdent,
-      Provider<CurrentUser> userProvider,
+      Provider<CurrentUser> user,
       ProjectsCollection projectsCollection,
-      CommitValidators.Factory commitValidatorsFactory,
       ChangeInserter.Factory changeInserterFactory,
       ChangeJson.Factory json,
       ChangeUtil changeUtil,
+      BatchUpdate.Factory updateFactory,
       @GerritServerConfig Config config) {
     this.db = db;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
-    this.userProvider = userProvider;
+    this.user = user;
     this.projectsCollection = projectsCollection;
-    this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeInserterFactory = changeInserterFactory;
     this.jsonFactory = json;
     this.changeUtil = changeUtil;
+    this.updateFactory = updateFactory;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
   }
 
   @Override
-  public Response<ChangeInfo> apply(TopLevelResource parent,
-      ChangeInfo input) throws AuthException, OrmException,
-      BadRequestException, UnprocessableEntityException, IOException,
-      InvalidChangeOperationException, ResourceNotFoundException,
-      MethodNotAllowedException, ResourceConflictException {
+  public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInfo input)
+      throws OrmException, IOException, InvalidChangeOperationException,
+      RestApiException, UpdateException {
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
     }
@@ -188,85 +181,45 @@
       RevCommit mergeTip = rw.parseCommit(parentCommit);
 
       Timestamp now = TimeUtil.nowTs();
-      IdentifiedUser me = (IdentifiedUser) userProvider.get();
+      IdentifiedUser me = user.get().asIdentifiedUser();
       PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
 
       ObjectId id = ChangeIdUtil.computeChangeId(mergeTip.getTree(),
           mergeTip, author, author, input.subject);
       String commitMessage = ChangeIdUtil.insertId(input.subject, id);
 
-      RevCommit c = newCommit(git, rw, author, mergeTip, commitMessage);
+      try (ObjectInserter oi = git.newObjectInserter()) {
+        RevCommit c = newCommit(oi, rw, author, mergeTip, commitMessage);
 
-      Change change = new Change(
-          getChangeId(id, c),
-          new Change.Id(db.get().nextChangeId()),
-          me.getAccountId(),
-          new Branch.NameKey(project, refName),
-          now);
+        Change change = new Change(
+            getChangeId(id, c),
+            new Change.Id(db.get().nextChangeId()),
+            me.getAccountId(),
+            new Branch.NameKey(project, refName),
+            now);
 
-      ChangeInserter ins =
-          changeInserterFactory.create(refControl.getProjectControl(),
-              change, c);
-
-      ChangeMessage msg = new ChangeMessage(new ChangeMessage.Key(change.getId(),
-          ChangeUtil.messageUUID(db.get())),
-          me.getAccountId(),
-          ins.getPatchSet().getCreatedOn(),
-          ins.getPatchSet().getId());
-      msg.setMessage(String.format("Uploaded patch set %s.",
-          ins.getPatchSet().getPatchSetId()));
-
-      ins.setMessage(msg);
-      validateCommit(git, refControl, c, me, ins);
-      updateRef(git, rw, c, change, ins.getPatchSet());
-
-      String topic = input.topic;
-      if (topic != null) {
-        topic = Strings.emptyToNull(topic.trim());
+        ChangeInserter ins = changeInserterFactory
+            .create(refControl, change, c)
+            .setValidatePolicy(CommitValidators.Policy.GERRIT);
+        ins.setMessage(String.format("Uploaded patch set %s.",
+            ins.getPatchSet().getPatchSetId()));
+        String topic = input.topic;
+        if (topic != null) {
+          topic = Strings.emptyToNull(topic.trim());
+        }
+        change.setTopic(topic);
+        ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
+        ins.setGroups(groups);
+        try (BatchUpdate bu = updateFactory.create(
+            db.get(), change.getProject(), me, now)) {
+          bu.setRepository(git, rw, oi);
+          bu.insertChange(ins);
+          bu.execute();
+        }
+        ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
+        return Response.created(json.format(change.getId()));
       }
-      change.setTopic(topic);
-      ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
-      ins.setGroups(groups);
-      ins.insert();
 
-      ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
-      return Response.created(json.format(change.getId()));
-    }
-  }
-
-  private void validateCommit(Repository git, RefControl refControl,
-      RevCommit c, IdentifiedUser me, ChangeInserter ins)
-      throws ResourceConflictException {
-    PatchSet newPatchSet = ins.getPatchSet();
-    CommitValidators commitValidators =
-        commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
-    CommitReceivedEvent commitReceivedEvent =
-        new CommitReceivedEvent(new ReceiveCommand(
-            ObjectId.zeroId(),
-            c.getId(),
-            newPatchSet.getRefName()),
-            refControl.getProjectControl().getProject(),
-            refControl.getRefName(),
-            c,
-            me);
-
-    try {
-      commitValidators.validateForGerritCommits(commitReceivedEvent);
-    } catch (CommitValidationException e) {
-      throw new ResourceConflictException(e.getMessage());
-    }
-  }
-
-  private static void updateRef(Repository git, RevWalk rw, RevCommit c,
-      Change change, PatchSet newPatchSet) throws IOException {
-    RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(c);
-    ru.disableRefLog();
-    if (ru.update(rw) != RefUpdate.Result.NEW) {
-      throw new IOException(String.format(
-          "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
-          change.getDest().getParentKey().get(), ru.getResult()));
     }
   }
 
@@ -279,20 +232,16 @@
     return changeKey;
   }
 
-  private static RevCommit newCommit(Repository git, RevWalk rw,
+  private static RevCommit newCommit(ObjectInserter oi, RevWalk rw,
       PersonIdent authorIdent, RevCommit mergeTip, String commitMessage)
       throws IOException {
-    RevCommit emptyCommit;
-    try (ObjectInserter oi = git.newObjectInserter()) {
-      CommitBuilder commit = new CommitBuilder();
-      commit.setTreeId(mergeTip.getTree().getId());
-      commit.setParentId(mergeTip);
-      commit.setAuthor(authorIdent);
-      commit.setCommitter(authorIdent);
-      commit.setMessage(commitMessage);
-      emptyCommit = rw.parseCommit(insert(oi, commit));
-    }
-    return emptyCommit;
+    CommitBuilder commit = new CommitBuilder();
+    commit.setTreeId(mergeTip.getTree().getId());
+    commit.setParentId(mergeTip);
+    commit.setAuthor(authorIdent);
+    commit.setCommitter(authorIdent);
+    commit.setMessage(commitMessage);
+    return rw.parseCommit(insert(oi, commit));
   }
 
   private static ObjectId insert(ObjectInserter inserter,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
index 9bb1f02..495e695 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.change.DeleteChangeEdit.Input;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -42,8 +41,7 @@
 
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException, IOException,
-      InvalidChangeOperationException {
+      throws AuthException, ResourceNotFoundException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
     if (edit.isPresent()) {
       editUtil.delete(edit.get());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index d9512eb..6c37252 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -111,7 +111,7 @@
         ChangeMessage changeMessage =
             new ChangeMessage(new ChangeMessage.Key(rsrc.getChange().getId(),
                 ChangeUtil.messageUUID(db)),
-                ((IdentifiedUser) control.getCurrentUser()).getAccountId(),
+                control.getUser().getAccountId(),
                 TimeUtil.nowTs(), rsrc.getChange().currentPatchSetId());
         changeMessage.setMessage(msg.toString());
         cmUtil.addChangeMessage(db, update, changeMessage);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
index 733ff0b..24c2f0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
@@ -30,8 +30,8 @@
 public class DownloadContent implements RestReadView<FileResource> {
   private final FileContentUtil fileContentUtil;
 
-  @Option(name = "--suffix")
-  private String suffix;
+  @Option(name = "--parent")
+  private Integer parent;
 
   @Inject
   DownloadContent(FileContentUtil fileContentUtil) {
@@ -47,6 +47,6 @@
         rsrc.getRevision().getControl().getProjectControl().getProjectState();
     ObjectId revstr = ObjectId.fromString(
         rsrc.getRevision().getPatchSet().getRevision().get());
-    return fileContentUtil.downloadContent(projectState, revstr, path, suffix);
+    return fileContentUtil.downloadContent(projectState, revstr, path, parent);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
index 006b1ec..3dc0c78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 
@@ -57,6 +56,6 @@
   }
 
   Account.Id getAuthorId() {
-    return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
+    return getControl().getUser().getAccountId();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index 122156b..6122609 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
@@ -128,7 +128,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     throw new OutOfScopeException("No user on email thread");
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 86ce61e..d145ddf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -16,7 +16,6 @@
 
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
-import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
@@ -126,14 +125,20 @@
   }
 
   public BinaryResult downloadContent(ProjectState project, ObjectId revstr,
-      String path, @Nullable String suffix)
+      String path, @Nullable Integer parent)
           throws ResourceNotFoundException, IOException {
-    suffix = Strings.emptyToNull(CharMatcher.inRange('a', 'z')
-        .retainFrom(Strings.nullToEmpty(suffix)));
-
     try (Repository repo = openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
+      String suffix = "new";
       RevCommit commit = rw.parseCommit(revstr);
+      if (parent != null && parent > 0) {
+        if (commit.getParentCount() == 1) {
+          suffix = "old";
+        } else {
+          suffix = "old" + parent;
+        }
+        commit = rw.parseCommit(commit.getParent(parent - 1));
+      }
       ObjectReader reader = rw.getObjectReader();
       TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
       if (tw == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 9ddb0ed..84f8a04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -63,6 +63,7 @@
       d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
           ? e.getChangeType().getCode() : null;
       d.oldPath = e.getOldName();
+      d.sizeDelta = e.getSizeDelta();
       if (e.getPatchType() == Patch.PatchType.BINARY) {
         d.binary = true;
       } else {
@@ -76,6 +77,7 @@
         // when the file was rewritten and too little content survived. Write
         // a single record with data from both sides.
         d.status = Patch.ChangeType.REWRITE.getCode();
+        d.sizeDelta = o.sizeDelta;
         if (o.binary != null && o.binary) {
           d.binary = true;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index e19b6a9..e28d796 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -207,7 +206,7 @@
         throw new AuthException("Authentication required");
       }
 
-      Account.Id userId = ((IdentifiedUser) user).getAccountId();
+      Account.Id userId = user.getAccountId();
       List<String> r = scan(userId, resource.getPatchSet().getId());
 
       if (r.isEmpty() && 1 < resource.getPatchSet().getPatchSetId()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 4fbb6a8..77f7bda 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
@@ -24,9 +24,8 @@
 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.ChangeUtil;
 import com.google.gerrit.server.CommonConverters;
-import com.google.gerrit.server.change.WalkSorter.PatchSetData;
+import com.google.gerrit.server.change.PatchSetAncestorSorter.PatchSetData;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.index.IndexCollection;
@@ -43,7 +42,6 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -54,7 +52,7 @@
   private final Provider<ReviewDb> db;
   private final GetRelatedByAncestors byAncestors;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final Provider<WalkSorter> sorter;
+  private final PatchSetAncestorSorter sorter;
   private final IndexCollection indexes;
   private final boolean byAncestorsOnly;
 
@@ -63,7 +61,7 @@
       @GerritServerConfig Config cfg,
       GetRelatedByAncestors byAncestors,
       Provider<InternalChangeQuery> queryProvider,
-      Provider<WalkSorter> sorter,
+      PatchSetAncestorSorter sorter,
       IndexCollection indexes) {
     this.db = db;
     this.byAncestors = byAncestors;
@@ -107,16 +105,14 @@
     }
     List<ChangeAndCommit> result = new ArrayList<>(cds.size());
 
-    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)) {
+    boolean isEdit = rsrc.getEdit().isPresent();
+    PatchSet basePs = isEdit
+        ? rsrc.getEdit().get().getBasePatchSet()
+        : rsrc.getPatchSet();
+    for (PatchSetData d : sorter.sort(cds, basePs)) {
       PatchSet ps = d.patchSet();
       RevCommit commit;
-      if (ps.getId().equals(editBaseId)) {
+      if (isEdit && ps.getId().equals(basePs.getId())) {
         // Replace base of an edit with the edit itself.
         ps = rsrc.getPatchSet();
         commit = rsrc.getEdit().get().getEditCommit();
@@ -147,40 +143,6 @@
     return result;
   }
 
-  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) {
-      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());
-    }
-    return result;
-  }
-
-  private static boolean hasAnyGroup(PatchSet ps, List<String> groups) {
-    if (ps.getGroups() == null) {
-      return false;
-    }
-    // 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;
-  }
-
   public static class RelatedInfo {
     public List<ChangeAndCommit> changes;
   }
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 a502a7d..6b08469 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -63,7 +63,7 @@
   @Override
   public String getETag(RevisionResource rsrc) {
     Hasher h = Hashing.md5().newHasher();
-    CurrentUser user = rsrc.getControl().getCurrentUser();
+    CurrentUser user = rsrc.getControl().getUser();
     try {
       rsrc.getChangeResource().prepareETag(h, user);
       h.putBoolean(Submit.wholeTopicEnabled(config));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
index 9f7eb93..eb260f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
@@ -16,56 +16,18 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.validators.HashtagValidationListener;
-import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
 
-import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-@Singleton
 public class HashtagsUtil {
   private static final CharMatcher LEADER =
       CharMatcher.whitespace().or(CharMatcher.is('#'));
   private static final String PATTERN = "(?:\\s|\\A)#[\\p{L}[0-9]-_]+";
 
-  private final ChangeUpdate.Factory updateFactory;
-  private final Provider<ReviewDb> dbProvider;
-  private final ChangeIndexer indexer;
-  private final ChangeHooks hooks;
-  private final DynamicSet<HashtagValidationListener> hashtagValidationListeners;
-
-  @Inject
-  HashtagsUtil(ChangeUpdate.Factory updateFactory,
-      Provider<ReviewDb> dbProvider,
-      ChangeIndexer indexer,
-      ChangeHooks hooks,
-      DynamicSet<HashtagValidationListener> hashtagValidationListeners) {
-    this.updateFactory = updateFactory;
-    this.dbProvider = dbProvider;
-    this.indexer = indexer;
-    this.hooks = hooks;
-    this.hashtagValidationListeners = hashtagValidationListeners;
-  }
-
   public static String cleanupHashtag(String hashtag) {
     hashtag = LEADER.trimLeadingFrom(hashtag);
     hashtag = CharMatcher.whitespace().trimTrailingFrom(hashtag);
@@ -83,7 +45,7 @@
     return result;
   }
 
-  private Set<String> extractTags(Set<String> input)
+  static Set<String> extractTags(Set<String> input)
       throws IllegalArgumentException {
     if (input == null) {
       return Collections.emptySet();
@@ -102,54 +64,6 @@
     }
   }
 
-  public TreeSet<String> setHashtags(ChangeControl control,
-      HashtagsInput input, boolean runHooks, boolean index)
-          throws IllegalArgumentException, IOException,
-          ValidationException, AuthException, OrmException {
-    if (input == null
-        || (input.add == null && input.remove == null)) {
-      throw new IllegalArgumentException("Hashtags are required");
-    }
-
-    if (!control.canEditHashtags()) {
-      throw new AuthException("Editing hashtags not permitted");
-    }
-    ChangeUpdate update = updateFactory.create(control);
-    ChangeNotes notes = control.getNotes().load();
-
-    Set<String> existingHashtags = notes.getHashtags();
-    Set<String> updatedHashtags = new HashSet<>();
-    Set<String> toAdd = new HashSet<>(extractTags(input.add));
-    Set<String> toRemove = new HashSet<>(extractTags(input.remove));
-
-    for (HashtagValidationListener validator : hashtagValidationListeners) {
-      validator.validateHashtags(update.getChange(), toAdd, toRemove);
-    }
-
-    if (existingHashtags != null && !existingHashtags.isEmpty()) {
-      updatedHashtags.addAll(existingHashtags);
-      toAdd.removeAll(existingHashtags);
-      toRemove.retainAll(existingHashtags);
-    }
-
-    if (toAdd.size() > 0 || toRemove.size() > 0) {
-      updatedHashtags.addAll(toAdd);
-      updatedHashtags.removeAll(toRemove);
-      update.setHashtags(updatedHashtags);
-      update.commit();
-
-      if (index) {
-        indexer.index(dbProvider.get(), update.getChange());
-      }
-
-      if (runHooks) {
-        IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
-        hooks.doHashtagsChangedHook(
-            update.getChange(), currentUser.getAccount(),
-            toAdd, toRemove, updatedHashtags,
-            dbProvider.get());
-      }
-    }
-    return new TreeSet<>(updatedHashtags);
+  private HashtagsUtil() {
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
index e63363e..a0be718 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -47,7 +47,7 @@
       throws IOException, AuthException {
     ChangeControl ctl = rsrc.getControl();
     if (!ctl.isOwner()
-        && !ctl.getCurrentUser().getCapabilities().canMaintainServer()) {
+        && !ctl.getUser().getCapabilities().canMaintainServer()) {
       throw new AuthException(
           "Only change owner or server maintainer can reindex");
     }
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
index 2b5d7d9..561a040 100644
--- 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
@@ -19,7 +19,6 @@
 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;
@@ -51,13 +50,12 @@
   @Override
   public Map<String, List<CommentInfo>> apply(
       ChangeResource rsrc) throws AuthException, OrmException {
-    if (!rsrc.getControl().getCurrentUser().isIdentifiedUser()) {
+    if (!rsrc.getControl().getUser().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());
+    List<PatchLineComment> drafts = plcUtil.draftByChangeAuthor(
+        db.get(), cd.notes(), rsrc.getControl().getUser().getAccountId());
     return commentJson.get()
         .setFillAccounts(false)
         .setFillPatchSet(true)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index e318162..8d92beb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -206,11 +207,11 @@
       Iterable<Ref> refs = Iterables.concat(
           refDatabase.getRefs(Constants.R_HEADS).values(),
           refDatabase.getRefs(Constants.R_TAGS).values());
-      try (RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+      try (CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
         RevFlag canMerge = rw.newFlag("CAN_MERGE");
-        CodeReviewCommit rev = parse(rw, key.commit);
+        CodeReviewCommit rev = rw.parseCommit(key.commit);
         rev.add(canMerge);
-        CodeReviewCommit tip = parse(rw, key.into);
+        CodeReviewCommit tip = rw.parseCommit(key.into);
         Set<RevCommit> accepted = alreadyAccepted(rw, refs);
         accepted.add(tip);
         accepted.addAll(Arrays.asList(rev.getParents()));
@@ -239,12 +240,6 @@
       }
       return accepted;
     }
-
-    private CodeReviewCommit parse(RevWalk rw, ObjectId id)
-        throws MissingObjectException, IncorrectObjectTypeException,
-        IOException {
-      return (CodeReviewCommit) rw.parseCommit(id);
-    }
   }
 
   public static class MergeabilityWeigher
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 99f8ba6..9c588ed 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
@@ -119,12 +119,14 @@
     get(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Get.class);
     get(CHANGE_EDIT_KIND, "meta").to(ChangeEdits.GetMeta.class);
 
-    factory(ReviewerResource.Factory.class);
     factory(AccountLoader.Factory.class);
-    factory(EmailReviewComments.Factory.class);
-    factory(ChangeInserter.Factory.class);
-    factory(PatchSetInserter.Factory.class);
     factory(ChangeEdits.Create.Factory.class);
     factory(ChangeEdits.DeleteFile.Factory.class);
+    factory(ChangeInserter.Factory.class);
+    factory(EmailReviewComments.Factory.class);
+    factory(PatchSetInserter.Factory.class);
+    factory(RebaseChangeOp.Factory.class);
+    factory(ReviewerResource.Factory.class);
+    factory(SetHashtagsOp.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetAncestorSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetAncestorSorter.java
new file mode 100644
index 0000000..8b0ceb2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetAncestorSorter.java
@@ -0,0 +1,250 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@Singleton
+class PatchSetAncestorSorter {
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  PatchSetAncestorSorter(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  public List<PatchSetData> sort(List<ChangeData> in, PatchSet startPs)
+      throws OrmException, IOException {
+    checkArgument(!in.isEmpty(), "Input may not be empty");
+    // Map of all patch sets, keyed by commit SHA-1.
+    Map<String, PatchSetData> byId = collectById(in);
+    PatchSetData start = byId.get(startPs.getRevision().get());
+    checkArgument(start != null, "%s not found in %s", startPs, in);
+    ProjectControl ctl = start.data().changeControl().getProjectControl();
+
+    // Map of patch set -> immediate parent.
+    ListMultimap<PatchSetData, PatchSetData> parents =
+        ArrayListMultimap.create(in.size(), 3);
+    // Map of patch set -> immediate children.
+    ListMultimap<PatchSetData, PatchSetData> children =
+        ArrayListMultimap.create(in.size(), 3);
+    // All other patch sets of the same change as startPs.
+    List<PatchSetData> otherPatchSetsOfStart = new ArrayList<>();
+
+    for (ChangeData cd : in) {
+      for (PatchSet ps : cd.patchSets()) {
+        PatchSetData thisPsd = checkNotNull(byId.get(ps.getRevision().get()));
+        if (cd.getId().equals(start.id()) && !ps.getId().equals(start.psId())) {
+          otherPatchSetsOfStart.add(thisPsd);
+        }
+        for (RevCommit p : thisPsd.commit().getParents()) {
+          PatchSetData parentPsd = byId.get(p.name());
+          if (parentPsd != null) {
+            parents.put(thisPsd, parentPsd);
+            children.put(parentPsd, thisPsd);
+          }
+        }
+      }
+    }
+
+    List<PatchSetData> ancestors = walkAncestors(ctl, parents, start);
+    List<PatchSetData> descendants =
+        walkDescendants(ctl, children, start, otherPatchSetsOfStart, ancestors);
+    List<PatchSetData> result =
+        new ArrayList<>(ancestors.size() + descendants.size() - 1);
+    result.addAll(Lists.reverse(descendants));
+    result.addAll(ancestors);
+    return result;
+  }
+
+  private Map<String, PatchSetData> collectById(List<ChangeData> in)
+      throws OrmException, IOException {
+    Project.NameKey project = in.get(0).change().getProject();
+    Map<String, PatchSetData> result =
+        Maps.newHashMapWithExpectedSize(in.size() * 3);
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      rw.setRetainBody(true);
+      for (ChangeData cd : in) {
+        checkArgument(cd.change().getProject().equals(project),
+            "Expected change %s in project %s, found %s",
+            cd.getId(), project, cd.change().getProject());
+        for (PatchSet ps : cd.patchSets()) {
+          String id = ps.getRevision().get();
+          RevCommit c = rw.parseCommit(ObjectId.fromString(id));
+          PatchSetData psd = PatchSetData.create(cd, ps, c);
+          result.put(id, psd);
+        }
+      }
+    }
+    return result;
+  }
+
+  private static List<PatchSetData> walkAncestors(ProjectControl ctl,
+      ListMultimap<PatchSetData, PatchSetData> parents, PatchSetData start)
+      throws OrmException {
+    List<PatchSetData> result = new ArrayList<>();
+    Deque<PatchSetData> pending = new ArrayDeque<>();
+    pending.add(start);
+    while (!pending.isEmpty()) {
+      PatchSetData psd = pending.remove();
+      if (!isVisible(psd, ctl)) {
+        continue;
+      }
+      result.add(psd);
+      pending.addAll(Lists.reverse(parents.get(psd)));
+    }
+    return result;
+  }
+
+  private static List<PatchSetData> walkDescendants(ProjectControl ctl,
+      ListMultimap<PatchSetData, PatchSetData> children,
+      PatchSetData start, List<PatchSetData> otherPatchSetsOfStart,
+      List<PatchSetData> ancestors)
+      throws OrmException {
+    Set<Change.Id> alreadyEmittedChanges = new HashSet<>();
+    addAllChangeIds(alreadyEmittedChanges, ancestors);
+
+    // Prefer descendants found by following the original patch set passed in.
+    List<PatchSetData> result = walkDescendentsImpl(
+        ctl, alreadyEmittedChanges, children, ImmutableList.of(start));
+    addAllChangeIds(alreadyEmittedChanges, result);
+
+    // Then, go back and add new indirect descendants found by following any
+    // other patch sets of start. These show up after all direct descendants,
+    // because we wouldn't know where in the walk to insert them.
+    result.addAll(walkDescendentsImpl(
+          ctl, alreadyEmittedChanges, children, otherPatchSetsOfStart));
+    return result;
+  }
+
+  private static void addAllChangeIds(Collection<Change.Id> changeIds,
+      Iterable<PatchSetData> psds) {
+    for (PatchSetData psd : psds) {
+      changeIds.add(psd.id());
+    }
+  }
+
+  private static List<PatchSetData> walkDescendentsImpl(ProjectControl ctl,
+      Set<Change.Id> alreadyEmittedChanges,
+      ListMultimap<PatchSetData, PatchSetData> children,
+      List<PatchSetData> start) throws OrmException {
+    if (start.isEmpty()) {
+      return ImmutableList.of();
+    }
+    Map<Change.Id, PatchSet.Id> maxPatchSetIds = new HashMap<>();
+    List<PatchSetData> allPatchSets = new ArrayList<>();
+    Deque<PatchSetData> pending = new ArrayDeque<>();
+    pending.addAll(start);
+    while (!pending.isEmpty()) {
+      PatchSetData psd = pending.remove();
+      if (!isVisible(psd, ctl)) {
+        continue;
+      }
+      if (!alreadyEmittedChanges.contains(psd.id())) {
+        // Don't emit anything for changes that were previously emitted, even
+        // though different patch sets might show up later. However, do
+        // continue walking through them for the purposes of finding indirect
+        // descendants.
+        PatchSet.Id oldMax = maxPatchSetIds.get(psd.id());
+        if (oldMax == null || psd.psId().get() > oldMax.get()) {
+          maxPatchSetIds.put(psd.id(), psd.psId());
+        }
+        allPatchSets.add(psd);
+      }
+      // Breadth-first search with oldest children first.
+      // TODO(dborowitz): After killing PatchSetAncestors, consider DFS to keep
+      // parallel history together.
+      pending.addAll(Lists.reverse(children.get(psd)));
+    }
+
+    // If we saw the same change multiple times, prefer the latest patch set.
+    List<PatchSetData> result = new ArrayList<>(allPatchSets.size());
+    for (PatchSetData psd : allPatchSets) {
+      if (checkNotNull(maxPatchSetIds.get(psd.id())).equals(psd.psId())) {
+        result.add(psd);
+      }
+    }
+    return result;
+  }
+
+  private static boolean isVisible(PatchSetData psd, ProjectControl ctl)
+      throws OrmException {
+    // Reuse existing project control rather than lazily creating a new one for
+    // each ChangeData.
+    return ctl.controlFor(psd.data().change())
+        .isPatchVisible(psd.patchSet(), psd.data());
+  }
+
+  @AutoValue
+  abstract static class PatchSetData {
+    @VisibleForTesting
+    static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
+      return new AutoValue_PatchSetAncestorSorter_PatchSetData(cd, ps, commit);
+    }
+
+    abstract ChangeData data();
+    abstract PatchSet patchSet();
+    abstract RevCommit commit();
+
+    PatchSet.Id psId() {
+      return patchSet().getId();
+    }
+
+    Change.Id id() {
+      return psId().getParentKey();
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(patchSet().getId(), commit());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 7ff1c89e..68f1497 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -32,35 +31,32 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.ReviewerState;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ChangeModifiedException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,132 +64,81 @@
 import java.io.IOException;
 import java.util.Collections;
 
-public class PatchSetInserter {
+public class PatchSetInserter extends BatchUpdate.Op {
   private static final Logger log =
       LoggerFactory.getLogger(PatchSetInserter.class);
 
   public static interface Factory {
-    PatchSetInserter create(Repository git, RevWalk revWalk, ChangeControl ctl,
+    PatchSetInserter create(RefControl refControl, PatchSet.Id psId,
         RevCommit commit);
   }
 
-  /**
-   * Whether to use {@link CommitValidators#validateForGerritCommits},
-   * {@link CommitValidators#validateForReceiveCommits}, or no commit
-   * validation.
-   */
-  public static enum ValidatePolicy {
-    GERRIT, RECEIVE_COMMITS, NONE
-  }
-
+  // Injected fields.
   private final ChangeHooks hooks;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ReviewDb db;
-  private final ChangeUpdate.Factory updateFactory;
-  private final ChangeControl.GenericFactory ctlFactory;
-  private final GitReferenceUpdated gitRefUpdated;
   private final CommitValidators.Factory commitValidatorsFactory;
-  private final ChangeIndexer indexer;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ApprovalCopier approvalCopier;
   private final ChangeMessagesUtil cmUtil;
 
-  private final Repository git;
-  private final RevWalk revWalk;
+  // Assisted-injected fields.
+  private final PatchSet.Id psId;
   private final RevCommit commit;
-  private final ChangeControl ctl;
-  private final IdentifiedUser user;
+  private final RefControl refControl;
 
-  private PatchSet patchSet;
-  private ChangeMessage changeMessage;
+  // Fields exposed as setters.
   private SshInfo sshInfo;
-  private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
+  private String message;
+  private CommitValidators.Policy validatePolicy =
+      CommitValidators.Policy.GERRIT;
   private boolean draft;
   private Iterable<String> groups;
-  private boolean runHooks;
-  private boolean sendMail;
+  private boolean runHooks = true;
+  private boolean sendMail = true;
   private Account.Id uploader;
   private boolean allowClosed;
 
-  @Inject
+  // Fields set during some phase of BatchUpdate.Op.
+  private Change change;
+  private PatchSet patchSet;
+  private PatchSetInfo patchSetInfo;
+  private ChangeMessage changeMessage;
+  private SetMultimap<ReviewerState, Account.Id> oldReviewers;
+
+  @AssistedInject
   public PatchSetInserter(ChangeHooks hooks,
       ReviewDb db,
-      ChangeUpdate.Factory updateFactory,
-      ChangeControl.GenericFactory ctlFactory,
       ApprovalsUtil approvalsUtil,
       ApprovalCopier approvalCopier,
       ChangeMessagesUtil cmUtil,
       PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
-      ChangeIndexer indexer,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
-      @Assisted Repository git,
-      @Assisted RevWalk revWalk,
-      @Assisted ChangeControl ctl,
+      @Assisted RefControl refControl,
+      @Assisted PatchSet.Id psId,
       @Assisted RevCommit commit) {
-    checkArgument(ctl.getCurrentUser().isIdentifiedUser(),
-        "only IdentifiedUser may create patch set on change %s",
-        ctl.getChange().getId());
     this.hooks = hooks;
     this.db = db;
-    this.updateFactory = updateFactory;
-    this.ctlFactory = ctlFactory;
     this.approvalsUtil = approvalsUtil;
     this.approvalCopier = approvalCopier;
     this.cmUtil = cmUtil;
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.indexer = indexer;
     this.replacePatchSetFactory = replacePatchSetFactory;
 
-    this.git = git;
-    this.revWalk = revWalk;
+    this.refControl = refControl;
+    this.psId = psId;
     this.commit = commit;
-    this.ctl = ctl;
-    this.user = (IdentifiedUser) ctl.getCurrentUser();
-    this.runHooks = true;
-    this.sendMail = true;
   }
 
-  public PatchSetInserter setPatchSet(PatchSet patchSet) {
-    Change c = ctl.getChange();
-    PatchSet.Id psid = patchSet.getId();
-    checkArgument(psid.getParentKey().equals(c.getId()),
-        "patch set %s not for change %s", psid, c.getId());
-    checkArgument(psid.get() > c.currentPatchSetId().get(),
-        "new patch set ID %s is not greater than current patch set ID %s",
-        psid.get(), c.currentPatchSetId().get());
-    this.patchSet = patchSet;
-    return this;
+  public PatchSet.Id getPatchSetId() {
+    return psId;
   }
 
-  public PatchSet.Id getPatchSetId() throws IOException {
-    init();
-    return patchSet.getId();
-  }
-
-  public PatchSet getPatchSet() {
-    checkState(patchSet != null,
-        "getPatchSet() only valid after patch set is created");
-    return patchSet;
-  }
-
-  public PatchSetInserter setMessage(String message)
-      throws OrmException, IOException {
-    init();
-    changeMessage = new ChangeMessage(
-        new ChangeMessage.Key(
-            ctl.getChange().getId(), ChangeUtil.messageUUID(db)),
-        user.getAccountId(), TimeUtil.nowTs(), patchSet.getId());
-    changeMessage.setMessage(message);
-    return this;
-  }
-
-  public PatchSetInserter setMessage(ChangeMessage changeMessage) {
-    this.changeMessage = changeMessage;
+  public PatchSetInserter setMessage(String message) {
+    this.message = message;
     return this;
   }
 
@@ -202,7 +147,7 @@
     return this;
   }
 
-  public PatchSetInserter setValidatePolicy(ValidatePolicy validate) {
+  public PatchSetInserter setValidatePolicy(CommitValidators.Policy validate) {
     this.validatePolicy = checkNotNull(validate);
     return this;
   }
@@ -237,161 +182,143 @@
     return this;
   }
 
-  public Change insert() throws InvalidChangeOperationException, OrmException,
-      IOException, NoSuchChangeException {
+  public Change getChange() {
+    checkState(change != null, "getChange() only valid after executing update");
+    return change;
+  }
+
+  public PatchSet getPatchSet() {
+    checkState(patchSet != null, "getPatchSet() only valid after executing update");
+    return patchSet;
+  }
+
+  @Override
+  public void updateRepo(RepoContext ctx)
+      throws InvalidChangeOperationException, IOException {
     init();
-    validate();
+    validate(ctx);
+    patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
+    ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(),
+        commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
+  }
 
-    Change c = ctl.getChange();
-    Change updatedChange;
-    RefUpdate ru = git.updateRef(patchSet.getRefName());
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(commit);
-    ru.disableRefLog();
-    if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-      throw new IOException(String.format(
-          "Failed to create ref %s in %s: %s", patchSet.getRefName(),
-          c.getDest().getParentKey().get(), ru.getResult()));
+  @Override
+  public void updateChange(ChangeContext ctx) throws OrmException,
+      InvalidChangeOperationException {
+    ChangeControl ctl = ctx.getChangeControl();
+
+    change = ctx.getChange();
+    Change.Id id = change.getId();
+    final PatchSet.Id currentPatchSetId = change.currentPatchSetId();
+    if (!change.getStatus().isOpen() && !allowClosed) {
+      throw new InvalidChangeOperationException(String.format(
+          "Change %s is closed", change.getId()));
     }
-    gitRefUpdated.fire(c.getProject(), ru);
 
-    final PatchSet.Id currentPatchSetId = c.currentPatchSetId();
+    patchSet = new PatchSet(psId);
+    patchSet.setCreatedOn(ctx.getWhen());
+    patchSet.setUploader(firstNonNull(uploader, ctl.getChange().getOwner()));
+    patchSet.setRevision(new RevId(commit.name()));
+    patchSet.setDraft(draft);
 
-    ChangeUpdate update = updateFactory.create(ctl, patchSet.getCreatedOn());
+    ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+    if (groups != null) {
+      patchSet.setGroups(groups);
+    } else {
+      patchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
+    }
+    db.patchSets().insert(Collections.singleton(patchSet));
 
-    db.changes().beginTransaction(c.getId());
-    try {
-      updatedChange = db.changes().get(c.getId());
-      if (!updatedChange.getStatus().isOpen() && !allowClosed) {
-        throw new InvalidChangeOperationException(String.format(
-            "Change %s is closed", c.getId()));
-      }
+    if (sendMail) {
+      oldReviewers = approvalsUtil.getReviewers(db, ctl.getNotes());
+    }
 
-      ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
-      if (groups != null) {
-        patchSet.setGroups(groups);
-      } else {
-        patchSet.setGroups(GroupCollector.getCurrentGroups(db, c));
-      }
-      db.patchSets().insert(Collections.singleton(patchSet));
+    if (message != null) {
+      changeMessage = new ChangeMessage(
+          new ChangeMessage.Key(
+              ctl.getChange().getId(), ChangeUtil.messageUUID(db)),
+          ctx.getUser().getAccountId(), ctx.getWhen(), patchSet.getId());
+      changeMessage.setMessage(message);
+    }
 
-      SetMultimap<ReviewerState, Account.Id> oldReviewers = sendMail
-          ? approvalsUtil.getReviewers(db, ctl.getNotes())
-          : null;
-
-      updatedChange =
-          db.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() {
-            @Override
-            public Change update(Change change) {
-              if (change.getStatus().isClosed() && !allowClosed) {
-                return null;
-              }
-              if (!change.currentPatchSetId().equals(currentPatchSetId)) {
-                return null;
-              }
-              if (change.getStatus() != Change.Status.DRAFT && !allowClosed) {
-                change.setStatus(Change.Status.NEW);
-              }
-              change.setCurrentPatchSet(patchSetInfoFactory.get(commit,
-                  patchSet.getId()));
-              ChangeUtil.updated(change);
-              return change;
-            }
-          });
-      if (updatedChange == null) {
-        throw new ChangeModifiedException(String.format(
-            "Change %s was modified", c.getId()));
-      }
-
-      if (messageIsForChange()) {
-        cmUtil.addChangeMessage(db, update, changeMessage);
-      }
-
-      approvalCopier.copy(db, ctl, patchSet);
-      db.commit();
-      if (messageIsForChange()) {
-        update.commit();
-      }
-
-      if (!messageIsForChange()) {
-        commitMessageNotForChange(updatedChange);
-      }
-
-      if (sendMail) {
-        try {
-          PatchSetInfo info = patchSetInfoFactory.get(commit, patchSet.getId());
-          ReplacePatchSetSender cm =
-              replacePatchSetFactory.create(c.getId());
-          cm.setFrom(user.getAccountId());
-          cm.setPatchSet(patchSet, info);
-          cm.setChangeMessage(changeMessage);
-          cm.addReviewers(oldReviewers.get(ReviewerState.REVIEWER));
-          cm.addExtraCC(oldReviewers.get(ReviewerState.CC));
-          cm.send();
-        } catch (Exception err) {
-          log.error("Cannot send email for new patch set on change "
-              + updatedChange.getId(), err);
+    // TODO(dborowitz): Throw ResourceConflictException instead of using
+    // AtomicUpdate.
+    change = db.changes().atomicUpdate(id, new AtomicUpdate<Change>() {
+      @Override
+      public Change update(Change change) {
+        if (change.getStatus().isClosed() && !allowClosed) {
+          return null;
         }
+        if (!change.currentPatchSetId().equals(currentPatchSetId)) {
+          return null;
+        }
+        if (change.getStatus() != Change.Status.DRAFT && !allowClosed) {
+          change.setStatus(Change.Status.NEW);
+        }
+        change.setCurrentPatchSet(patchSetInfo);
+        ChangeUtil.updated(change);
+        return change;
       }
-
-    } finally {
-      db.rollback();
+    });
+    if (change == null) {
+      throw new ChangeModifiedException(String.format(
+          "Change %s was modified", id));
     }
-    indexer.index(db, updatedChange);
-    if (runHooks) {
-      hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
-    }
-    return updatedChange;
-  }
 
-  private void commitMessageNotForChange(Change updatedChange)
-      throws OrmException, NoSuchChangeException, IOException {
+    approvalCopier.copy(db, ctl, patchSet);
     if (changeMessage != null) {
-      Change otherChange =
-          db.changes().get(changeMessage.getPatchSetId().getParentKey());
-      ChangeControl otherControl =
-          ctlFactory.controlFor(otherChange, user);
-      ChangeUpdate updateForOtherChange =
-          updateFactory.create(otherControl, updatedChange.getLastUpdatedOn());
-      cmUtil.addChangeMessage(db, updateForOtherChange, changeMessage);
-      updateForOtherChange.commit();
+      cmUtil.addChangeMessage(db, ctx.getChangeUpdate(), changeMessage);
     }
   }
 
-  private void init() throws IOException {
+  @Override
+  public void postUpdate(Context ctx) throws OrmException {
+    if (sendMail) {
+      try {
+        ReplacePatchSetSender cm = replacePatchSetFactory.create(
+            change.getId());
+        cm.setFrom(ctx.getUser().getAccountId());
+        cm.setPatchSet(patchSet, patchSetInfo);
+        cm.setChangeMessage(changeMessage);
+        cm.addReviewers(oldReviewers.get(ReviewerState.REVIEWER));
+        cm.addExtraCC(oldReviewers.get(ReviewerState.CC));
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot send email for new patch set on change "
+            + change.getId(), err);
+      }
+    }
+
+    if (runHooks) {
+      hooks.doPatchsetCreatedHook(change, patchSet, ctx.getDb());
+    }
+  }
+
+  private void init() {
     if (sshInfo == null) {
       sshInfo = new NoSshInfo();
     }
-    if (patchSet == null) {
-      patchSet = new PatchSet(
-          ChangeUtil.nextPatchSetId(git, ctl.getChange().currentPatchSetId()));
-      patchSet.setCreatedOn(TimeUtil.nowTs());
-      patchSet.setUploader(ctl.getChange().getOwner());
-      patchSet.setRevision(new RevId(commit.name()));
-    }
-    patchSet.setDraft(draft);
-    if (uploader != null) {
-      patchSet.setUploader(uploader);
-    }
   }
 
-  private void validate() throws InvalidChangeOperationException, IOException {
-    CommitValidators cv =
-        commitValidatorsFactory.create(ctl.getRefControl(), sshInfo, git);
+  private void validate(RepoContext ctx)
+      throws InvalidChangeOperationException, IOException {
+    CommitValidators cv = commitValidatorsFactory.create(
+        refControl, sshInfo, ctx.getRepository());
 
-    String refName = patchSet.getRefName();
+    String refName = getPatchSetId().toRefName();
     CommitReceivedEvent event = new CommitReceivedEvent(
         new ReceiveCommand(
             ObjectId.zeroId(),
             commit.getId(),
             refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
-        ctl.getProjectControl().getProject(), ctl.getRefControl().getRefName(),
-        commit, user);
+        refControl.getProjectControl().getProject(), refControl.getRefName(),
+        commit, ctx.getUser().asIdentifiedUser());
 
     try {
       switch (validatePolicy) {
       case RECEIVE_COMMITS:
-        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(git, revWalk);
+        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(
+            ctx.getRepository(), ctx.getRevWalk());
         cv.validateForReceiveCommits(event, rejectCommits);
         break;
       case GERRIT:
@@ -404,9 +331,4 @@
       throw new InvalidChangeOperationException(e.getMessage());
     }
   }
-
-  private boolean messageIsForChange() {
-    return changeMessage != null && changeMessage.getKey().getParentKey()
-        .equals(patchSet.getId().getParentKey());
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index 62520f4..a3fc2e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -14,43 +14,47 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.io.IOException;
-import java.util.Set;
-
 @Singleton
-public class PostHashtags implements RestModifyView<ChangeResource, HashtagsInput>,
-    UiAction<ChangeResource>{
-  private HashtagsUtil hashtagsUtil;
+public class PostHashtags
+    implements RestModifyView<ChangeResource, HashtagsInput>,
+    UiAction<ChangeResource> {
+  private final Provider<ReviewDb> db;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final SetHashtagsOp.Factory hashtagsFactory;
 
   @Inject
-  PostHashtags(HashtagsUtil hashtagsUtil) {
-    this.hashtagsUtil = hashtagsUtil;
+  PostHashtags(Provider<ReviewDb> db,
+      BatchUpdate.Factory batchUpdateFactory,
+      SetHashtagsOp.Factory hashtagsFactory) {
+    this.db = db;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.hashtagsFactory = hashtagsFactory;
   }
 
   @Override
-  public Response<Set<String>> apply(ChangeResource req, HashtagsInput input)
-      throws AuthException, OrmException, IOException, BadRequestException,
-      ResourceConflictException {
-
-    try {
-      return Response.<Set<String>> ok(hashtagsUtil.setHashtags(
-          req.getControl(), input, true, true));
-    } catch (IllegalArgumentException e) {
-      throw new BadRequestException(e.getMessage());
-    } catch (ValidationException e) {
-      throw new ResourceConflictException(e.getMessage());
+  public Response<ImmutableSortedSet<String>> apply(ChangeResource req,
+      HashtagsInput input) throws RestApiException, UpdateException {
+    try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+          req.getChange().getProject(), req.getControl().getUser(),
+          TimeUtil.nowTs())) {
+      SetHashtagsOp op = hashtagsFactory.create(input);
+      bu.addOp(req.getChange().getId(), op);
+      bu.execute();
+      return Response.<ImmutableSortedSet<String>> ok(op.getUpdatedHashtags());
     }
   }
 
@@ -60,4 +64,4 @@
       .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 e10077a..7a24dbc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -36,6 +36,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
@@ -44,6 +45,7 @@
 import com.google.gerrit.reviewdb.client.CommentRange;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -52,7 +54,10 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountsCollection;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
@@ -61,18 +66,21 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+@Singleton
 public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
   private static final Logger log = LoggerFactory.getLogger(PostReview.class);
 
@@ -81,47 +89,37 @@
   }
 
   private final Provider<ReviewDb> db;
+  private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangesCollection changes;
   private final ChangeData.Factory changeDataFactory;
-  private final ChangeUpdate.Factory updateFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
   private final PatchLineCommentsUtil plcUtil;
   private final PatchListCache patchListCache;
-  private final ChangeIndexer indexer;
   private final AccountsCollection accounts;
   private final EmailReviewComments.Factory email;
-  @Deprecated private final ChangeHooks hooks;
-
-  private Change change;
-  private ChangeMessage message;
-  private Timestamp timestamp;
-  private List<PatchLineComment> comments = Lists.newArrayList();
-  private List<String> labelDelta = Lists.newArrayList();
-  private Map<String, Short> categories = Maps.newHashMap();
+  private final ChangeHooks hooks;
 
   @Inject
   PostReview(Provider<ReviewDb> db,
+      BatchUpdate.Factory batchUpdateFactory,
       ChangesCollection changes,
       ChangeData.Factory changeDataFactory,
-      ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
       PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
-      ChangeIndexer indexer,
       AccountsCollection accounts,
       EmailReviewComments.Factory email,
       ChangeHooks hooks) {
     this.db = db;
+    this.batchUpdateFactory = batchUpdateFactory;
     this.changes = changes;
     this.changeDataFactory = changeDataFactory;
-    this.updateFactory = updateFactory;
     this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.indexer = indexer;
     this.accounts = accounts;
     this.email = email;
     this.hooks = hooks;
@@ -129,16 +127,12 @@
 
   @Override
   public Output apply(RevisionResource revision, ReviewInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      UnprocessableEntityException, OrmException, IOException {
+      throws RestApiException, UpdateException, OrmException {
     return apply(revision, input, TimeUtil.nowTs());
   }
 
   public Output apply(RevisionResource revision, ReviewInput input,
-      Timestamp ts) throws AuthException, BadRequestException,
-      ResourceConflictException, UnprocessableEntityException, OrmException,
-      IOException {
-    timestamp = ts;
+      Timestamp ts) throws RestApiException, UpdateException, OrmException {
     if (revision.getEdit().isPresent()) {
       throw new ResourceConflictException("cannot post review on edit");
     }
@@ -156,46 +150,15 @@
       input.notify = NotifyHandling.NONE;
     }
 
-    db.get().changes().beginTransaction(revision.getChange().getId());
-    boolean dirty = false;
-    try {
-      change = db.get().changes().get(revision.getChange().getId());
-      if (change.getLastUpdatedOn().before(timestamp)) {
-        change.setLastUpdatedOn(timestamp);
-      }
-
-      ChangeUpdate update = updateFactory.create(revision.getControl(), timestamp);
-      update.setPatchSetId(revision.getPatchSet().getId());
-      dirty |= insertComments(revision, update, input.comments, input.drafts);
-      dirty |= updateLabels(revision, update, input.labels);
-      dirty |= insertMessage(revision, input.message, update);
-      if (dirty) {
-        db.get().changes().update(Collections.singleton(change));
-        db.get().commit();
-      }
-      update.commit();
-    } finally {
-      db.get().rollback();
+    try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+          revision.getChange().getProject(), revision.getUser(), ts)) {
+      bu.addOp(
+          revision.getChange().getId(),
+          new Op(revision.getPatchSet().getId(), input));
+      bu.execute();
     }
-
-    if (dirty) {
-      indexer.index(db.get(), change);
-    }
-    if (message != null && input.notify.compareTo(NotifyHandling.NONE) > 0) {
-      email.create(
-          input.notify,
-          change,
-          revision.getPatchSet(),
-          revision.getAccountId(),
-          message,
-          comments).sendAsync();
-    }
-
     Output output = new Output();
     output.labels = input.labels;
-    if (message != null) {
-      fireCommentAddedHook(revision);
-    }
     return output;
   }
 
@@ -336,258 +299,302 @@
     }
   }
 
-  private boolean insertComments(RevisionResource rsrc,
-      ChangeUpdate update, Map<String, List<CommentInput>> in, DraftHandling draftsHandling)
-      throws OrmException {
-    if (in == null) {
-      in = Collections.emptyMap();
+  private class Op extends BatchUpdate.Op {
+    private final PatchSet.Id psId;
+    private final ReviewInput in;
+
+    private IdentifiedUser user;
+    private Change change;
+    private PatchSet ps;
+    private ChangeMessage message;
+    private List<PatchLineComment> comments = new ArrayList<>();
+    private List<String> labelDelta = new ArrayList<>();
+    private Map<String, Short> categories = new HashMap<>();
+
+    private Op(PatchSet.Id psId, ReviewInput in) {
+      this.psId = psId;
+      this.in = in;
     }
 
-    Map<String, PatchLineComment> drafts = Collections.emptyMap();
-    if (!in.isEmpty() || draftsHandling != DraftHandling.KEEP) {
-      if (draftsHandling == DraftHandling.PUBLISH_ALL_REVISIONS) {
-        drafts = changeDrafts(rsrc);
-      } else {
-        drafts = patchSetDrafts(rsrc);
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException {
+      user = ctx.getUser().asIdentifiedUser();
+      change = ctx.getChange();
+      if (change.getLastUpdatedOn().before(ctx.getWhen())) {
+        change.setLastUpdatedOn(ctx.getWhen());
+      }
+      ps = ctx.getDb().patchSets().get(psId);
+      ctx.getChangeUpdate().setPatchSetId(psId);
+      boolean dirty = false;
+      dirty |= insertComments(ctx);
+      dirty |= updateLabels(ctx);
+      dirty |= insertMessage(ctx);
+      if (dirty) {
+        ctx.getDb().changes().update(Collections.singleton(change));
       }
     }
 
-    List<PatchLineComment> del = Lists.newArrayList();
-    List<PatchLineComment> ups = Lists.newArrayList();
-
-    for (Map.Entry<String, List<CommentInput>> ent : in.entrySet()) {
-      String path = ent.getKey();
-      for (CommentInput c : ent.getValue()) {
-        String parent = Url.decode(c.inReplyTo);
-        PatchLineComment e = drafts.remove(Url.decode(c.id));
-        if (e == null) {
-          e = new PatchLineComment(
-              new PatchLineComment.Key(
-                  new Patch.Key(rsrc.getPatchSet().getId(), path),
-                  ChangeUtil.messageUUID(db.get())),
-              c.line != null ? c.line : 0,
-              rsrc.getAccountId(),
-              parent, timestamp);
-        } else if (parent != null) {
-          e.setParentUuid(parent);
-        }
-        e.setStatus(PatchLineComment.Status.PUBLISHED);
-        e.setWrittenOn(timestamp);
-        e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
-        setCommentRevId(e, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
-        e.setMessage(c.message);
-        if (c.range != null) {
-          e.setRange(new CommentRange(
-              c.range.startLine,
-              c.range.startCharacter,
-              c.range.endLine,
-              c.range.endCharacter));
-          e.setLine(c.range.endLine);
-        }
-        ups.add(e);
+    @Override
+    public void postUpdate(Context ctx) {
+      if (message == null) {
+        return;
+      }
+      if (in.notify.compareTo(NotifyHandling.NONE) > 0) {
+        email.create(
+            in.notify,
+            change,
+            ps,
+            user.getAccountId(),
+            message,
+            comments).sendAsync();
+      }
+      try {
+        hooks.doCommentAddedHook(change, user.getAccount(), ps,
+            message.getMessage(), categories, ctx.getDb());
+      } catch (OrmException e) {
+        log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
       }
     }
 
-    switch (MoreObjects.firstNonNull(draftsHandling, DraftHandling.DELETE)) {
-      case KEEP:
-      default:
-        break;
-      case DELETE:
-        del.addAll(drafts.values());
-        break;
-      case PUBLISH:
-      case PUBLISH_ALL_REVISIONS:
-        for (PatchLineComment e : drafts.values()) {
+    private boolean insertComments(ChangeContext ctx) throws OrmException {
+      Map<String, List<CommentInput>> map = in.comments;
+      if (map == null) {
+        map = Collections.emptyMap();
+      }
+
+      Map<String, PatchLineComment> drafts = Collections.emptyMap();
+      if (!map.isEmpty() || in.drafts != DraftHandling.KEEP) {
+        if (in.drafts == DraftHandling.PUBLISH_ALL_REVISIONS) {
+          drafts = changeDrafts(ctx);
+        } else {
+          drafts = patchSetDrafts(ctx);
+        }
+      }
+
+      List<PatchLineComment> del = Lists.newArrayList();
+      List<PatchLineComment> ups = Lists.newArrayList();
+
+      for (Map.Entry<String, List<CommentInput>> ent : map.entrySet()) {
+        String path = ent.getKey();
+        for (CommentInput c : ent.getValue()) {
+          String parent = Url.decode(c.inReplyTo);
+          PatchLineComment e = drafts.remove(Url.decode(c.id));
+          if (e == null) {
+            e = new PatchLineComment(
+                new PatchLineComment.Key(
+                    new Patch.Key(psId, path),
+                    ChangeUtil.messageUUID(ctx.getDb())),
+                c.line != null ? c.line : 0,
+                user.getAccountId(),
+                parent, ctx.getWhen());
+          } else if (parent != null) {
+            e.setParentUuid(parent);
+          }
           e.setStatus(PatchLineComment.Status.PUBLISHED);
-          e.setWrittenOn(timestamp);
-          setCommentRevId(e, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
+          e.setWrittenOn(ctx.getWhen());
+          e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
+          setCommentRevId(e, patchListCache, ctx.getChange(), ps);
+          e.setMessage(c.message);
+          if (c.range != null) {
+            e.setRange(new CommentRange(
+                c.range.startLine,
+                c.range.startCharacter,
+                c.range.endLine,
+                c.range.endCharacter));
+            e.setLine(c.range.endLine);
+          }
           ups.add(e);
         }
-        break;
-    }
-    plcUtil.deleteComments(db.get(), update, del);
-    plcUtil.upsertComments(db.get(), update, ups);
-    comments.addAll(ups);
-    return !del.isEmpty() || !ups.isEmpty();
-  }
-
-  private Map<String, PatchLineComment> 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())) {
-      drafts.put(c.getKey().get(), c);
-    }
-    return drafts;
-  }
-
-  private boolean updateLabels(RevisionResource rsrc, ChangeUpdate update,
-      Map<String, Short> labels) throws OrmException {
-    if (labels == null) {
-      labels = Collections.emptyMap();
-    }
-
-    List<PatchSetApproval> del = Lists.newArrayList();
-    List<PatchSetApproval> ups = Lists.newArrayList();
-    Map<String, PatchSetApproval> current = scanLabels(rsrc, del);
-
-    LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
-    for (Map.Entry<String, Short> ent : labels.entrySet()) {
-      String name = ent.getKey();
-      LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
-      if (change.getStatus().isClosed()) {
-        // TODO Allow updating some labels even when closed.
-        continue;
       }
 
-      PatchSetApproval c = current.remove(lt.getName());
-      String normName = lt.getName();
-      if (ent.getValue() == null || ent.getValue() == 0) {
-        // User requested delete of this label.
-        if (c != null) {
-          if (c.getValue() != 0) {
-            addLabelDelta(normName, (short) 0);
+      switch (firstNonNull(in.drafts, DraftHandling.DELETE)) {
+        case KEEP:
+        default:
+          break;
+        case DELETE:
+          del.addAll(drafts.values());
+          break;
+        case PUBLISH:
+        case PUBLISH_ALL_REVISIONS:
+          for (PatchLineComment e : drafts.values()) {
+            e.setStatus(PatchLineComment.Status.PUBLISHED);
+            e.setWrittenOn(ctx.getWhen());
+            setCommentRevId(e, patchListCache, ctx.getChange(), ps);
+            ups.add(e);
           }
-          del.add(c);
-          update.putApproval(ent.getKey(), (short) 0);
+          break;
+      }
+      plcUtil.deleteComments(ctx.getDb(), ctx.getChangeUpdate(), del);
+      plcUtil.upsertComments(ctx.getDb(), ctx.getChangeUpdate(), ups);
+      comments.addAll(ups);
+      return !del.isEmpty() || !ups.isEmpty();
+    }
+
+    private Map<String, PatchLineComment> changeDrafts(ChangeContext ctx)
+        throws OrmException {
+      Map<String, PatchLineComment> drafts = Maps.newHashMap();
+      for (PatchLineComment c : plcUtil.draftByChangeAuthor(
+          ctx.getDb(), ctx.getChangeNotes(), user.getAccountId())) {
+        drafts.put(c.getKey().get(), c);
+      }
+      return drafts;
+    }
+
+    private Map<String, PatchLineComment> patchSetDrafts(ChangeContext ctx)
+        throws OrmException {
+      Map<String, PatchLineComment> drafts = Maps.newHashMap();
+      for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(ctx.getDb(),
+          psId, user.getAccountId(), ctx.getChangeNotes())) {
+        drafts.put(c.getKey().get(), c);
+      }
+      return drafts;
+    }
+
+    private boolean updateLabels(ChangeContext ctx) throws OrmException {
+      Map<String, Short> labels = in.labels;
+      if (labels == null) {
+        labels = Collections.emptyMap();
+      }
+
+      List<PatchSetApproval> del = Lists.newArrayList();
+      List<PatchSetApproval> ups = Lists.newArrayList();
+      Map<String, PatchSetApproval> current = scanLabels(ctx, del);
+
+      ChangeUpdate update = ctx.getChangeUpdate();
+      LabelTypes labelTypes = ctx.getChangeControl().getLabelTypes();
+      for (Map.Entry<String, Short> ent : labels.entrySet()) {
+        String name = ent.getKey();
+        LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
+        if (ctx.getChange().getStatus().isClosed()) {
+          // TODO Allow updating some labels even when closed.
+          continue;
         }
-      } else if (c != null && c.getValue() != ent.getValue()) {
-        c.setValue(ent.getValue());
-        c.setGranted(timestamp);
-        ups.add(c);
-        addLabelDelta(normName, c.getValue());
-        categories.put(normName, c.getValue());
-        update.putApproval(ent.getKey(), ent.getValue());
-      } else if (c != null && c.getValue() == ent.getValue()) {
-        current.put(normName, c);
-      } else if (c == null) {
-        c = new PatchSetApproval(new PatchSetApproval.Key(
-                rsrc.getPatchSet().getId(),
-                rsrc.getAccountId(),
-                lt.getLabelId()),
-            ent.getValue(), TimeUtil.nowTs());
-        c.setGranted(timestamp);
-        ups.add(c);
-        addLabelDelta(normName, c.getValue());
-        categories.put(normName, c.getValue());
-        update.putApproval(ent.getKey(), ent.getValue());
+
+        PatchSetApproval c = current.remove(lt.getName());
+        String normName = lt.getName();
+        if (ent.getValue() == null || ent.getValue() == 0) {
+          // User requested delete of this label.
+          if (c != null) {
+            if (c.getValue() != 0) {
+              addLabelDelta(normName, (short) 0);
+            }
+            del.add(c);
+            update.putApproval(ent.getKey(), (short) 0);
+          }
+        } else if (c != null && c.getValue() != ent.getValue()) {
+          c.setValue(ent.getValue());
+          c.setGranted(ctx.getWhen());
+          ups.add(c);
+          addLabelDelta(normName, c.getValue());
+          categories.put(normName, c.getValue());
+          update.putApproval(ent.getKey(), ent.getValue());
+        } else if (c != null && c.getValue() == ent.getValue()) {
+          current.put(normName, c);
+        } else if (c == null) {
+          c = new PatchSetApproval(new PatchSetApproval.Key(
+                  psId,
+                  user.getAccountId(),
+                  lt.getLabelId()),
+              ent.getValue(), TimeUtil.nowTs());
+          c.setGranted(ctx.getWhen());
+          ups.add(c);
+          addLabelDelta(normName, c.getValue());
+          categories.put(normName, c.getValue());
+          update.putApproval(ent.getKey(), ent.getValue());
+        }
+      }
+
+      forceCallerAsReviewer(ctx, current, ups, del);
+      ctx.getDb().patchSetApprovals().delete(del);
+      ctx.getDb().patchSetApprovals().upsert(ups);
+      return !del.isEmpty() || !ups.isEmpty();
+    }
+
+    private void forceCallerAsReviewer(ChangeContext ctx,
+        Map<String, PatchSetApproval> current, List<PatchSetApproval> ups,
+        List<PatchSetApproval> del) {
+      if (current.isEmpty() && ups.isEmpty()) {
+        // TODO Find another way to link reviewers to changes.
+        if (del.isEmpty()) {
+          // If no existing label is being set to 0, hack in the caller
+          // as a reviewer by picking the first server-wide LabelType.
+          PatchSetApproval c = new PatchSetApproval(new PatchSetApproval.Key(
+              psId,
+              user.getAccountId(),
+              ctx.getChangeControl().getLabelTypes().getLabelTypes().get(0)
+                  .getLabelId()),
+              (short) 0, TimeUtil.nowTs());
+          c.setGranted(ctx.getWhen());
+          ups.add(c);
+        } else {
+          // Pick a random label that is about to be deleted and keep it.
+          Iterator<PatchSetApproval> i = del.iterator();
+          PatchSetApproval c = i.next();
+          c.setValue((short) 0);
+          c.setGranted(ctx.getWhen());
+          i.remove();
+          ups.add(c);
+        }
       }
     }
 
-    forceCallerAsReviewer(rsrc, current, ups, del);
-    db.get().patchSetApprovals().delete(del);
-    db.get().patchSetApprovals().upsert(ups);
-    return !del.isEmpty() || !ups.isEmpty();
-  }
+    private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx,
+        List<PatchSetApproval> del) throws OrmException {
+      LabelTypes labelTypes = ctx.getChangeControl().getLabelTypes();
+      Map<String, PatchSetApproval> current = Maps.newHashMap();
 
-  private void forceCallerAsReviewer(RevisionResource rsrc,
-      Map<String, PatchSetApproval> current, List<PatchSetApproval> ups,
-      List<PatchSetApproval> del) {
-    if (current.isEmpty() && ups.isEmpty()) {
-      // TODO Find another way to link reviewers to changes.
-      if (del.isEmpty()) {
-        // If no existing label is being set to 0, hack in the caller
-        // as a reviewer by picking the first server-wide LabelType.
-        PatchSetApproval c = new PatchSetApproval(new PatchSetApproval.Key(
-            rsrc.getPatchSet().getId(),
-            rsrc.getAccountId(),
-            rsrc.getControl().getLabelTypes().getLabelTypes().get(0)
-                .getLabelId()),
-            (short) 0, TimeUtil.nowTs());
-        c.setGranted(timestamp);
-        ups.add(c);
-      } else {
-        // Pick a random label that is about to be deleted and keep it.
-        Iterator<PatchSetApproval> i = del.iterator();
-        PatchSetApproval c = i.next();
-        c.setValue((short) 0);
-        c.setGranted(timestamp);
-        i.remove();
-        ups.add(c);
+      for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
+          ctx.getDb(), ctx.getChangeControl(), psId, user.getAccountId())) {
+        if (a.isSubmit()) {
+          continue;
+        }
+
+        LabelType lt = labelTypes.byLabel(a.getLabelId());
+        if (lt != null) {
+          current.put(lt.getName(), a);
+        } else {
+          del.add(a);
+        }
       }
+      return current;
     }
-  }
 
-  private Map<String, PatchSetApproval> scanLabels(RevisionResource rsrc,
-      List<PatchSetApproval> del) throws OrmException {
-    LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
-    Map<String, PatchSetApproval> current = Maps.newHashMap();
+    private boolean insertMessage(ChangeContext ctx)
+        throws OrmException {
+      String msg = Strings.nullToEmpty(in.message).trim();
 
-    for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
-        db.get(), rsrc.getControl(), rsrc.getPatchSet().getId(),
-        rsrc.getAccountId())) {
-      if (a.isSubmit()) {
-        continue;
+      StringBuilder buf = new StringBuilder();
+      for (String d : labelDelta) {
+        buf.append(" ").append(d);
+      }
+      if (comments.size() == 1) {
+        buf.append("\n\n(1 comment)");
+      } else if (comments.size() > 1) {
+        buf.append(String.format("\n\n(%d comments)", comments.size()));
+      }
+      if (!msg.isEmpty()) {
+        buf.append("\n\n").append(msg);
+      }
+      if (buf.length() == 0) {
+        return false;
       }
 
-      LabelType lt = labelTypes.byLabel(a.getLabelId());
-      if (lt != null) {
-        current.put(lt.getName(), a);
-      } else {
-        del.add(a);
-      }
-    }
-    return current;
-  }
-
-  private void addLabelDelta(String name, short value) {
-    labelDelta.add(LabelVote.create(name, value).format());
-  }
-
-  private boolean insertMessage(RevisionResource rsrc, String msg,
-      ChangeUpdate update) throws OrmException {
-    msg = Strings.nullToEmpty(msg).trim();
-
-    StringBuilder buf = new StringBuilder();
-    for (String d : labelDelta) {
-      buf.append(" ").append(d);
-    }
-    if (comments.size() == 1) {
-      buf.append("\n\n(1 comment)");
-    } else if (comments.size() > 1) {
-      buf.append(String.format("\n\n(%d comments)", comments.size()));
-    }
-    if (!msg.isEmpty()) {
-      buf.append("\n\n").append(msg);
-    }
-    if (buf.length() == 0) {
-      return false;
+      message = new ChangeMessage(
+          new ChangeMessage.Key(
+            psId.getParentKey(), ChangeUtil.messageUUID(ctx.getDb())),
+          user.getAccountId(),
+          ctx.getWhen(),
+          psId);
+      message.setMessage(String.format(
+          "Patch Set %d:%s",
+          psId.get(),
+          buf.toString()));
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), message);
+      return true;
     }
 
-    message = new ChangeMessage(
-        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db.get())),
-        rsrc.getAccountId(),
-        timestamp,
-        rsrc.getPatchSet().getId());
-    message.setMessage(String.format(
-        "Patch Set %d:%s",
-        rsrc.getPatchSet().getPatchSetId(),
-        buf.toString()));
-    cmUtil.addChangeMessage(db.get(), update, message);
-    return true;
-  }
-
-  @Deprecated
-  private void fireCommentAddedHook(RevisionResource rsrc) {
-    IdentifiedUser user = (IdentifiedUser) rsrc.getControl().getCurrentUser();
-    try {
-      hooks.doCommentAddedHook(change,
-          user.getAccount(),
-          rsrc.getPatchSet(),
-          message.getMessage(),
-          categories, db.get());
-    } catch (OrmException e) {
-      log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
+    private void addLabelDelta(String name, short value) {
+      labelDelta.add(LabelVote.create(name, value).format());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index fc27010..3ab84ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -21,7 +21,6 @@
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,7 +35,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
@@ -84,7 +82,7 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeUpdate.Factory updateFactory;
-  private final Provider<CurrentUser> currentUser;
+  private final Provider<IdentifiedUser> user;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
   private final ChangeHooks hooks;
@@ -102,7 +100,7 @@
       AccountLoader.Factory accountLoaderFactory,
       Provider<ReviewDb> db,
       ChangeUpdate.Factory updateFactory,
-      Provider<CurrentUser> currentUser,
+      Provider<IdentifiedUser> user,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritServerConfig Config cfg,
       ChangeHooks hooks,
@@ -118,7 +116,7 @@
     this.accountLoaderFactory = accountLoaderFactory;
     this.dbProvider = db;
     this.updateFactory = updateFactory;
-    this.currentUser = currentUser;
+    this.user = user;
     this.identifiedUserFactory = identifiedUserFactory;
     this.cfg = cfg;
     this.hooks = hooks;
@@ -130,7 +128,7 @@
   @Override
   public PostResult apply(ChangeResource rsrc, AddReviewerInput input)
       throws AuthException, BadRequestException, UnprocessableEntityException,
-      OrmException, EmailException, IOException {
+      OrmException, IOException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
@@ -175,7 +173,7 @@
     ChangeControl control = rsrc.getControl();
     Set<Account> members;
     try {
-      members = groupMembersFactory.create(control.getCurrentUser()).listAccounts(
+      members = groupMembersFactory.create(control.getUser()).listAccounts(
               group.getGroupUUID(), control.getProject().getNameKey());
     } catch (NoSuchGroupException e) {
       throw new UnprocessableEntityException(e.getMessage());
@@ -275,16 +273,16 @@
     //
     // The user knows they added themselves, don't bother emailing them.
     List<Account.Id> toMail = Lists.newArrayListWithCapacity(added.size());
-    IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
+    Account.Id userId = user.get().getAccountId();
     for (PatchSetApproval psa : added) {
-      if (!psa.getAccountId().equals(identifiedUser.getAccountId())) {
+      if (!psa.getAccountId().equals(userId)) {
         toMail.add(psa.getAccountId());
       }
     }
     if (!toMail.isEmpty()) {
       try {
         AddReviewerSender cm = addReviewerSenderFactory.create(change.getId());
-        cm.setFrom(identifiedUser.getAccountId());
+        cm.setFrom(userId);
         cm.addReviewers(toMail);
         cm.send();
       } catch (Exception err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index b88931e..e137ac4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -29,7 +29,8 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -83,8 +84,8 @@
 
     @Override
     public Response<?> apply(ChangeResource rsrc, Publish.Input in)
-        throws AuthException, ResourceConflictException, NoSuchChangeException,
-        IOException, OrmException {
+        throws NoSuchProjectException, IOException, OrmException,
+        RestApiException, UpdateException {
       Capable r =
           rsrc.getControl().getProjectControl().canPushToAtLeastOneRef();
       if (r != Capable.OK) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index dd9a44d..622c99d 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,112 +14,111 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.PublishDraftPatchSet.Input;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.mail.PatchSetNotificationSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 @Singleton
 public class PublishDraftPatchSet implements RestModifyView<RevisionResource, Input>,
     UiAction<RevisionResource> {
+  private static final Logger log =
+      LoggerFactory.getLogger(PublishDraftPatchSet.class);
+
   public static class Input {
   }
 
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeUpdate.Factory updateFactory;
-  private final PatchSetNotificationSender sender;
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeHooks hooks;
-  private final ChangeIndexer indexer;
+  private final ApprovalsUtil approvalsUtil;
+  private final AccountResolver accountResolver;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final CreateChangeSender.Factory createChangeSenderFactory;
+  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
 
   @Inject
-  public PublishDraftPatchSet(Provider<ReviewDb> dbProvider,
-      ChangeUpdate.Factory updateFactory,
-      PatchSetNotificationSender sender,
+  public PublishDraftPatchSet(
+      Provider<ReviewDb> dbProvider,
+      BatchUpdate.Factory updateFactory,
       ChangeHooks hooks,
-      ChangeIndexer indexer) {
+      ApprovalsUtil approvalsUtil,
+      AccountResolver accountResolver,
+      PatchSetInfoFactory patchSetInfoFactory,
+      CreateChangeSender.Factory createChangeSenderFactory,
+      ReplacePatchSetSender.Factory replacePatchSetFactory) {
     this.dbProvider = dbProvider;
     this.updateFactory = updateFactory;
-    this.sender = sender;
     this.hooks = hooks;
-    this.indexer = indexer;
+    this.approvalsUtil = approvalsUtil;
+    this.accountResolver = accountResolver;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.createChangeSenderFactory = createChangeSenderFactory;
+    this.replacePatchSetFactory = replacePatchSetFactory;
   }
 
   @Override
   public Response<?> apply(RevisionResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException,
-      ResourceConflictException, OrmException, IOException {
-    if (!rsrc.getPatchSet().isDraft()) {
-      throw new ResourceConflictException("Patch set is not a draft");
+      throws RestApiException, UpdateException {
+    return apply(rsrc.getUser(), rsrc.getChange(), rsrc.getPatchSet().getId(),
+        rsrc.getPatchSet());
+  }
+
+  private Response<?> apply(CurrentUser u, Change c, PatchSet.Id psId,
+      PatchSet ps) throws RestApiException, UpdateException {
+    try (BatchUpdate bu = updateFactory.create(
+        dbProvider.get(), c.getProject(), u, TimeUtil.nowTs())) {
+      bu.addOp(c.getId(), new Op(psId, ps));
+      bu.execute();
     }
-
-    if (!rsrc.getControl().canPublish(dbProvider.get())) {
-      throw new AuthException("Cannot publish this draft patch set");
-    }
-
-    PatchSet updatedPatchSet = updateDraftPatchSet(rsrc);
-    Change updatedChange = updateDraftChange(rsrc);
-    ChangeUpdate update = updateFactory.create(rsrc.getControl(),
-        updatedChange.getLastUpdatedOn());
-
-    if (!updatedPatchSet.isDraft()
-        || updatedChange.getStatus() == Change.Status.NEW) {
-      indexer.index(dbProvider.get(), updatedChange);
-      sender.send(rsrc.getNotes(), update,
-          rsrc.getChange().getStatus() == Change.Status.DRAFT,
-          rsrc.getUser(), updatedChange, updatedPatchSet,
-          rsrc.getControl().getLabelTypes());
-      hooks.doDraftPublishedHook(updatedChange, updatedPatchSet,
-          dbProvider.get());
-    }
-
     return Response.none();
   }
 
-  private Change updateDraftChange(RevisionResource rsrc) throws OrmException {
-    return dbProvider.get().changes()
-        .atomicUpdate(rsrc.getChange().getId(),
-        new AtomicUpdate<Change>() {
-      @Override
-      public Change update(Change change) {
-        if (change.getStatus() == Change.Status.DRAFT) {
-          change.setStatus(Change.Status.NEW);
-          ChangeUtil.updated(change);
-        }
-        return change;
-      }
-    });
-  }
-
-  private PatchSet updateDraftPatchSet(RevisionResource rsrc) throws OrmException {
-    return dbProvider.get().patchSets()
-        .atomicUpdate(rsrc.getPatchSet().getId(),
-        new AtomicUpdate<PatchSet>() {
-      @Override
-      public PatchSet update(PatchSet patchset) {
-        patchset.setDraft(false);
-        return patchset;
-      }
-    });
-  }
-
   @Override
   public UiAction.Description getDescription(RevisionResource rsrc) {
     try {
@@ -135,28 +134,140 @@
 
   public static class CurrentRevision implements
       RestModifyView<ChangeResource, Input> {
-    private final Provider<ReviewDb> dbProvider;
     private final PublishDraftPatchSet publish;
 
     @Inject
-    CurrentRevision(Provider<ReviewDb> dbProvider,
-        PublishDraftPatchSet publish) {
-      this.dbProvider = dbProvider;
+    CurrentRevision(PublishDraftPatchSet publish) {
       this.publish = publish;
     }
 
     @Override
     public Response<?> apply(ChangeResource rsrc, Input input)
-        throws AuthException, ResourceConflictException,
-        ResourceNotFoundException, IOException, OrmException {
-      PatchSet ps = dbProvider.get().patchSets()
-        .get(rsrc.getChange().currentPatchSetId());
+        throws RestApiException, UpdateException {
+      return publish.apply(rsrc.getControl().getUser(), rsrc.getChange(),
+          rsrc.getChange().currentPatchSetId(), null);
+    }
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final PatchSet.Id psId;
+
+    private PatchSet patchSet;
+    private Change change;
+    private boolean wasDraftChange;
+    private RevCommit commit;
+    private PatchSetInfo patchSetInfo;
+    private MailRecipients recipients;
+
+    private Op(PatchSet.Id psId, @Nullable PatchSet patchSet) {
+      this.psId = psId;
+      this.patchSet = patchSet;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx)
+        throws RestApiException, OrmException, IOException {
+      PatchSet ps = patchSet;
       if (ps == null) {
-        throw new ResourceConflictException("current revision is missing");
-      } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
-        throw new AuthException("current revision not accessible");
+        // Don't save in patchSet, since we're not in a transaction. Here we
+        // just need the revision, which is immutable.
+        ps = ctx.getDb().patchSets().get(psId);
+        if (ps == null) {
+          throw new ResourceNotFoundException(psId.toString());
+        }
       }
-      return publish.apply(new RevisionResource(rsrc, ps), input);
+      commit = ctx.getRevWalk().parseCommit(
+          ObjectId.fromString(ps.getRevision().get()));
+      patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx)
+        throws RestApiException, OrmException {
+      if (!ctx.getChangeControl().canPublish(ctx.getDb())) {
+        throw new AuthException("Cannot publish this draft patch set");
+      }
+      saveChange(ctx);
+      savePatchSet(ctx);
+      addReviewers(ctx);
+    }
+
+    private void saveChange(ChangeContext ctx) throws OrmException {
+      change = ctx.getChange();
+      wasDraftChange = change.getStatus() == Change.Status.DRAFT;
+      if (wasDraftChange) {
+        change.setStatus(Change.Status.NEW);
+        ChangeUtil.updated(change);
+        ctx.getDb().changes().update(Collections.singleton(change));
+      }
+    }
+
+    private void savePatchSet(ChangeContext ctx)
+        throws RestApiException, OrmException {
+      patchSet = ctx.getDb().patchSets().get(psId);
+      if (!patchSet.isDraft()) {
+        throw new ResourceConflictException("Patch set is not a draft");
+      }
+      patchSet.setDraft(false);
+    }
+
+    private void addReviewers(ChangeContext ctx) throws OrmException {
+      LabelTypes labelTypes = ctx.getChangeControl().getLabelTypes();
+      Collection<Account.Id> oldReviewers = approvalsUtil.getReviewers(
+          ctx.getDb(), ctx.getChangeNotes()).values();
+      List<FooterLine> footerLines = commit.getFooterLines();
+      recipients =
+          getRecipientsFromFooters(accountResolver, patchSet, footerLines);
+      recipients.remove(ctx.getUser().getAccountId());
+      approvalsUtil.addReviewers(ctx.getDb(), ctx.getChangeUpdate(), labelTypes,
+          change, patchSet, patchSetInfo, recipients.getReviewers(),
+          oldReviewers);
+    }
+
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      hooks.doDraftPublishedHook(change, patchSet, ctx.getDb());
+      if (patchSet.isDraft() && change.getStatus() == Change.Status.DRAFT) {
+        // Skip emails if the patch set is still a draft.
+        return;
+      }
+      try {
+        if (wasDraftChange) {
+          sendCreateChange(ctx);
+        } else {
+          sendReplacePatchSet(ctx);
+        }
+      } catch (EmailException | OrmException e) {
+        log.error("Cannot send email for publishing draft " + psId, e);
+      }
+    }
+
+    private void sendCreateChange(Context ctx) throws EmailException {
+      CreateChangeSender cm =
+          createChangeSenderFactory.create(change.getId());
+      cm.setFrom(ctx.getUser().getAccountId());
+      cm.setPatchSet(patchSet, patchSetInfo);
+      cm.addReviewers(recipients.getReviewers());
+      cm.addExtraCC(recipients.getCcOnly());
+      cm.send();
+    }
+
+    private void sendReplacePatchSet(Context ctx)
+        throws EmailException, OrmException {
+      Account.Id accountId = ctx.getUser().getAccountId();
+      ChangeMessage msg =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())), accountId,
+              ctx.getWhen(), psId);
+      msg.setMessage("Uploaded patch set " + psId.get() + ".");
+      ReplacePatchSetSender cm =
+          replacePatchSetFactory.create(change.getId());
+      cm.setFrom(accountId);
+      cm.setPatchSet(patchSet, patchSetInfo);
+      cm.setChangeMessage(msg);
+      cm.addReviewers(recipients.getReviewers());
+      cm.addExtraCC(recipients.getCcOnly());
+      cm.send();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index d171785..1061d4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
@@ -29,25 +30,25 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.PutTopic.Input;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.io.IOException;
+import java.util.Collections;
 
 @Singleton
 public class PutTopic implements RestModifyView<ChangeResource, Input>,
     UiAction<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeIndexer indexer;
   private final ChangeHooks hooks;
-  private final ChangeUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   public static class Input {
     @DefaultInput
@@ -55,80 +56,86 @@
   }
 
   @Inject
-  PutTopic(Provider<ReviewDb> dbProvider, ChangeIndexer indexer,
-      ChangeHooks hooks, ChangeUpdate.Factory updateFactory,
-      ChangeMessagesUtil cmUtil) {
+  PutTopic(Provider<ReviewDb> dbProvider,
+      ChangeHooks hooks,
+      ChangeMessagesUtil cmUtil,
+      BatchUpdate.Factory batchUpdateFactory) {
     this.dbProvider = dbProvider;
-    this.indexer = indexer;
     this.hooks = hooks;
-    this.updateFactory = updateFactory;
     this.cmUtil = cmUtil;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   @Override
   public Response<String> apply(ChangeResource req, Input input)
-      throws AuthException, OrmException, IOException {
-    if (input == null) {
-      input = new Input();
-    }
-
-    ChangeControl control = req.getControl();
-    Change change = req.getChange();
-    if (!control.canEditTopicName()) {
+      throws UpdateException, RestApiException {
+    ChangeControl ctl = req.getControl();
+    if (!ctl.canEditTopicName()) {
       throw new AuthException("changing topic not permitted");
     }
 
-    ReviewDb db = dbProvider.get();
-    final String newTopicName = Strings.nullToEmpty(input.topic);
-    String oldTopicName = Strings.nullToEmpty(change.getTopic());
-    if (!oldTopicName.equals(newTopicName)) {
+    Op op = new Op(ctl, input != null ? input : new Input());
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+      u.addOp(req.getChange().getId(), op);
+      u.execute();
+    }
+    return Strings.isNullOrEmpty(op.newTopicName)
+        ? Response.<String> none()
+        : Response.ok(op.newTopicName);
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final Input input;
+    private final IdentifiedUser caller;
+
+    private Change change;
+    private String oldTopicName;
+    private String newTopicName;
+
+    public Op(ChangeControl ctl, Input input) {
+      this.input = input;
+      this.caller = ctl.getUser().asIdentifiedUser();
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException {
+      change = ctx.getChange();
+      String newTopicName = Strings.nullToEmpty(input.topic);
+      String oldTopicName = Strings.nullToEmpty(change.getTopic());
+      if (oldTopicName.equals(newTopicName)) {
+        return;
+      }
       String summary;
       if (oldTopicName.isEmpty()) {
         summary = "Topic set to " + newTopicName;
       } else if (newTopicName.isEmpty()) {
         summary = "Topic " + oldTopicName + " removed";
       } else {
-        summary = String.format(
-            "Topic changed from %s to %s",
+        summary = String.format("Topic changed from %s to %s",
             oldTopicName, newTopicName);
       }
+      change.setTopic(Strings.emptyToNull(newTopicName));
+      ChangeUtil.updated(change);
+      ctx.getDb().changes().update(Collections.singleton(change));
 
-      IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
       ChangeMessage cmsg = new ChangeMessage(
-          new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
-          currentUser.getAccountId(), TimeUtil.nowTs(),
+          new ChangeMessage.Key(
+              change.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())),
+          caller.getAccountId(), ctx.getWhen(),
           change.currentPatchSetId());
       cmsg.setMessage(summary);
-      ChangeUpdate update;
-
-      db.changes().beginTransaction(change.getId());
-      try {
-        change = db.changes().atomicUpdate(change.getId(),
-          new AtomicUpdate<Change>() {
-            @Override
-            public Change update(Change change) {
-              change.setTopic(Strings.emptyToNull(newTopicName));
-              ChangeUtil.updated(change);
-              return change;
-            }
-          });
-
-        //TODO(yyonas): atomic update was not propagated
-        update = updateFactory.create(control);
-        cmUtil.addChangeMessage(db, update, cmsg);
-
-        db.commit();
-      } finally {
-        db.rollback();
-      }
-      update.commit();
-      indexer.index(db, change);
-      hooks.doTopicChangedHook(change, currentUser.getAccount(),
-          oldTopicName, db);
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), cmsg);
     }
-    return Strings.isNullOrEmpty(newTopicName)
-        ? Response.<String>none()
-        : Response.ok(newTopicName);
+
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      if (change != null) {
+        hooks.doTopicChangedHook(change, caller.getAccount(),
+            oldTopicName, ctx.getDb());
+      }
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index ece6752..60f285f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -15,31 +15,34 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -57,30 +60,36 @@
       ListChangesOption.CURRENT_REVISION,
       ListChangesOption.CURRENT_COMMIT);
 
+  private final BatchUpdate.Factory updateFactory;
   private final GitRepositoryManager repoManager;
-  private final Provider<RebaseChange> rebaseChange;
+  private final RebaseChangeOp.Factory rebaseFactory;
   private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  public Rebase(GitRepositoryManager repoManager,
-      Provider<RebaseChange> rebaseChange,
+  public Rebase(BatchUpdate.Factory updateFactory,
+      GitRepositoryManager repoManager,
+      RebaseChangeOp.Factory rebaseFactory,
       ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider) {
+    this.updateFactory = updateFactory;
     this.repoManager = repoManager;
-    this.rebaseChange = rebaseChange;
+    this.rebaseFactory = rebaseFactory;
     this.json = json;
     this.dbProvider = dbProvider;
   }
 
   @Override
   public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
-      throws AuthException, ResourceNotFoundException,
-      ResourceConflictException, EmailException, OrmException, IOException {
+      throws EmailException, OrmException, UpdateException, RestApiException,
+      IOException {
     ChangeControl control = rsrc.getControl();
     Change change = rsrc.getChange();
     try (Repository repo = repoManager.openRepository(change.getProject());
-        RevWalk rw = new RevWalk(repo)) {
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter oi = repo.newObjectInserter();
+        BatchUpdate bu = updateFactory.create(dbProvider.get(),
+          change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       if (!control.canRebase()) {
         throw new AuthException("rebase not permitted");
       } else if (!change.getStatus().isOpen()) {
@@ -90,13 +99,15 @@
         throw new ResourceConflictException(
             "cannot rebase merge commits or commit with no ancestor");
       }
-      rebaseChange.get().rebase(repo, rw, rsrc, findBaseRev(rw, rsrc, input));
-    } catch (InvalidChangeOperationException e) {
-      throw new ResourceConflictException(e.getMessage());
-    } catch (NoSuchChangeException e) {
-      throw new ResourceNotFoundException(change.getId().toString());
+      bu.setRepository(repo, rw, oi);
+      bu.addOp(change.getId(), rebaseFactory.create(
+            control, rsrc.getPatchSet(),
+            findBaseRev(rw, rsrc, input))
+          .setForceContentMerge(true)
+          .setRunHooks(true)
+          .setValidatePolicy(CommitValidators.Policy.GERRIT));
+      bu.execute();
     }
-
     return json.create(OPTIONS).format(change.getId());
   }
 
@@ -196,29 +207,30 @@
 
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
-    Project.NameKey project = resource.getChange().getProject();
+    PatchSet patchSet = resource.getPatchSet();
+    Branch.NameKey dest = resource.getChange().getDest();
     boolean visible = resource.getChange().getStatus().isOpen()
           && resource.isCurrent()
           && resource.getControl().canRebase();
+    boolean enabled = true;
+
     if (visible) {
-      try (Repository repo = repoManager.openRepository(project);
+      try (Repository repo = repoManager.openRepository(dest.getParentKey());
           RevWalk rw = new RevWalk(repo)) {
         visible = hasOneParent(rw, resource.getPatchSet());
+        enabled =
+            RebaseUtil.canRebase(patchSet, dest, repo, rw, dbProvider.get());
       } catch (IOException e) {
-        log.error("Failed to get ancestors of patch set "
-            + resource.getPatchSet().getId(), e);
+        log.error("Failed to check if patch set can be rebased: "
+            + resource.getPatchSet(), e);
         visible = false;
       }
     }
     UiAction.Description descr = new UiAction.Description()
       .setLabel("Rebase")
       .setTitle("Rebase onto tip of branch or parent change")
-      .setVisible(visible);
-    if (descr.isVisible()) {
-      // Disable the rebase button in the RebaseDialog if
-      // the change cannot be rebased.
-      descr.setEnabled(rebaseChange.get().canRebase(resource));
-    }
+      .setVisible(visible)
+      .setEnabled(enabled);
     return descr;
   }
 
@@ -233,8 +245,8 @@
 
     @Override
     public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
-        throws AuthException, ResourceNotFoundException,
-        ResourceConflictException, EmailException, OrmException, IOException {
+        throws EmailException, OrmException, UpdateException, RestApiException,
+        IOException {
       PatchSet ps =
           rebase.dbProvider.get().patchSets()
               .get(rsrc.getChange().currentPatchSetId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
deleted file mode 100644
index 030aa92..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
+++ /dev/null
@@ -1,356 +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.change;
-
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.TimeZone;
-
-@Singleton
-public class RebaseChange {
-  private static final Logger log = LoggerFactory.getLogger(RebaseChange.class);
-
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager gitManager;
-  private final TimeZone serverTimeZone;
-  private final MergeUtil.Factory mergeUtilFactory;
-  private final PatchSetInserter.Factory patchSetInserterFactory;
-
-  @Inject
-  RebaseChange(ChangeControl.GenericFactory changeControlFactory,
-      Provider<ReviewDb> db,
-      @GerritPersonIdent PersonIdent myIdent,
-      GitRepositoryManager gitManager,
-      MergeUtil.Factory mergeUtilFactory,
-      PatchSetInserter.Factory patchSetInserterFactory) {
-    this.changeControlFactory = changeControlFactory;
-    this.db = db;
-    this.gitManager = gitManager;
-    this.serverTimeZone = myIdent.getTimeZone();
-    this.mergeUtilFactory = mergeUtilFactory;
-    this.patchSetInserterFactory = patchSetInserterFactory;
-  }
-
-  /**
-   * Rebase the change of the given patch set.
-   * <p>
-   * If the patch set has no dependency to an open change, then the change is
-   * rebased on the tip of the destination branch.
-   * <p>
-   * If the patch set depends on an open change, it is rebased on the latest
-   * patch set of this change.
-   * <p>
-   * The rebased commit is added as new patch set to the change.
-   * <p>
-   * E-mail notification and triggering of hooks happens for the creation of the
-   * new patch set.
-   *
-   * @param git the repository.
-   * @param rw the RevWalk.
-   * @param rsrc revision to rebase.
-   * @param newBaseRev the commit that should be the new base.
-   * @throws NoSuchChangeException if the change to which the patch set belongs
-   *     does not exist or is not visible to the user.
-   * @throws EmailException if sending the e-mail to notify about the new patch
-   *     set fails.
-   * @throws OrmException if accessing the database fails.
-   * @throws IOException if accessing the repository fails.
-   * @throws InvalidChangeOperationException if rebase is not possible or not
-   *     allowed.
-   */
-  public void rebase(Repository git, RevWalk rw, RevisionResource rsrc,
-      String newBaseRev) throws NoSuchChangeException, EmailException,
-          OrmException, IOException, ResourceConflictException,
-          InvalidChangeOperationException {
-    Change change = rsrc.getChange();
-    PatchSet patchSet = rsrc.getPatchSet();
-    IdentifiedUser uploader = (IdentifiedUser) rsrc.getControl().getCurrentUser();
-
-    try (ObjectInserter inserter = git.newObjectInserter()) {
-      String baseRev = newBaseRev;
-      if (baseRev == null) {
-        baseRev = findBaseRevision(patchSet, change.getDest(), git, rw);
-      }
-
-      ObjectId baseObjectId = git.resolve(baseRev);
-      if (baseObjectId == null) {
-        throw new InvalidChangeOperationException(
-          "Cannot rebase: Failed to resolve baseRev: " + baseRev);
-      }
-
-      RevCommit baseCommit = rw.parseCommit(baseObjectId);
-      PersonIdent committerIdent =
-          uploader.newCommitterIdent(TimeUtil.nowTs(), serverTimeZone);
-
-      rebase(git, rw, inserter, change, patchSet.getId(),
-          uploader, baseCommit, mergeUtilFactory.create(
-              rsrc.getControl().getProjectControl().getProjectState(), true),
-          committerIdent, true, ValidatePolicy.GERRIT);
-    } catch (MergeConflictException e) {
-      throw new ResourceConflictException(e.getMessage());
-    }
-  }
-
-  /**
-   * Find the commit onto which a patch set should be rebased.
-   * <p>
-   * This is defined as the latest patch set of the change corresponding to
-   * this commit's parent, or the destination branch tip in the case where the
-   * parent's change is merged.
-   *
-   * @param patchSet patch set for which the new base commit should be found.
-   * @param destBranch the destination branch.
-   * @param git the repository.
-   * @param rw the RevWalk.
-   * @return the commit onto which the patch set should be rebased.
-   * @throws InvalidChangeOperationException if rebase is not possible or not
-   *     allowed.
-   * @throws IOException if accessing the repository fails.
-   * @throws OrmException if accessing the database fails.
-   */
-  private String findBaseRevision(PatchSet patchSet,
-      Branch.NameKey destBranch, Repository git, RevWalk rw)
-      throws InvalidChangeOperationException, IOException, OrmException {
-    String baseRev = null;
-    RevCommit commit = rw.parseCommit(
-        ObjectId.fromString(patchSet.getRevision().get()));
-
-    if (commit.getParentCount() > 1) {
-      throw new InvalidChangeOperationException(
-          "Cannot rebase a change with multiple parents.");
-    } else if (commit.getParentCount() == 0) {
-      throw new InvalidChangeOperationException(
-          "Cannot rebase a change without any parents"
-          + " (is this the initial commit?).");
-    }
-
-    RevId parentRev = new RevId(commit.getParent(0).name());
-
-    for (PatchSet depPatchSet : db.get().patchSets().byRevision(parentRev)) {
-      Change.Id depChangeId = depPatchSet.getId().getParentKey();
-      Change depChange = db.get().changes().get(depChangeId);
-      if (!depChange.getDest().equals(destBranch)) {
-        continue;
-      }
-
-      if (depChange.getStatus() == Status.ABANDONED) {
-        throw new InvalidChangeOperationException(
-            "Cannot rebase a change with an abandoned parent: "
-            + depChange.getKey());
-      }
-
-      if (depChange.getStatus().isOpen()) {
-        if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
-          throw new InvalidChangeOperationException(
-              "Change is already based on the latest patch set of the"
-              + " dependent change.");
-        }
-        PatchSet latestDepPatchSet =
-            db.get().patchSets().get(depChange.currentPatchSetId());
-        baseRev = latestDepPatchSet.getRevision().get();
-      }
-      break;
-    }
-
-    if (baseRev == null) {
-      // We are dependent on a merged PatchSet or have no PatchSet
-      // dependencies at all.
-      Ref destRef = git.getRefDatabase().exactRef(destBranch.get());
-      if (destRef == null) {
-        throw new InvalidChangeOperationException(
-            "The destination branch does not exist: " + destBranch.get());
-      }
-      baseRev = destRef.getObjectId().getName();
-      if (baseRev.equals(parentRev.get())) {
-        throw new InvalidChangeOperationException(
-            "Change is already up to date.");
-      }
-    }
-    return baseRev;
-  }
-
-  /**
-   * Rebase the change of the given patch set on the given base commit.
-   * <p>
-   * The rebased commit is added as new patch set to the change.
-   * <p>
-   * E-mail notification and triggering of hooks is only done for the creation
-   * of the new patch set if {@code sendEmail} and {@code runHooks} are true,
-   * respectively.
-   *
-   * @param git the repository.
-   * @param inserter the object inserter.
-   * @param change the change to rebase.
-   * @param patchSetId the patch set ID to rebase.
-   * @param uploader the user that creates the rebased patch set.
-   * @param baseCommit the commit that should be the new base.
-   * @param mergeUtil merge utilities for the destination project.
-   * @param committerIdent the committer's identity.
-   * @param runHooks if hooks should be run for the new patch set.
-   * @param validate if commit validation should be run for the new patch set.
-   * @param rw the RevWalk.
-   * @return the new patch set, which is based on the given base commit.
-   * @throws NoSuchChangeException if the change to which the patch set belongs
-   *     does not exist or is not visible to the user.
-   * @throws OrmException if accessing the database fails.
-   * @throws IOException if rebase is not possible.
-   * @throws InvalidChangeOperationException if rebase is not possible or not
-   *     allowed.
-   */
-  public PatchSet rebase(Repository git, RevWalk rw,
-      ObjectInserter inserter, Change change, PatchSet.Id patchSetId,
-      IdentifiedUser uploader, RevCommit baseCommit, MergeUtil mergeUtil,
-      PersonIdent committerIdent, boolean runHooks, ValidatePolicy validate)
-      throws NoSuchChangeException, OrmException, IOException,
-      InvalidChangeOperationException, MergeConflictException {
-    if (!change.currentPatchSetId().equals(patchSetId)) {
-      throw new InvalidChangeOperationException("patch set is not current");
-    }
-    PatchSet originalPatchSet = db.get().patchSets().get(patchSetId);
-
-    RevCommit rebasedCommit;
-    ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
-    ObjectId newId = rebaseCommit(git, inserter, rw.parseCommit(oldId),
-        baseCommit, mergeUtil, committerIdent);
-
-    rebasedCommit = rw.parseCommit(newId);
-
-    ChangeControl changeControl =
-        changeControlFactory.validateFor(change, uploader);
-
-    PatchSetInserter patchSetInserter = patchSetInserterFactory
-        .create(git, rw, changeControl, rebasedCommit)
-        .setValidatePolicy(validate)
-        .setDraft(originalPatchSet.isDraft())
-        .setUploader(uploader.getAccountId())
-        .setSendMail(false)
-        .setRunHooks(runHooks);
-
-    PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
-    ChangeMessage cmsg = new ChangeMessage(
-        new ChangeMessage.Key(change.getId(),
-            ChangeUtil.messageUUID(db.get())), uploader.getAccountId(),
-            TimeUtil.nowTs(), patchSetId);
-
-    cmsg.setMessage("Patch Set " + newPatchSetId.get()
-        + ": Patch Set " + patchSetId.get() + " was rebased");
-
-    Change newChange = patchSetInserter
-        .setMessage(cmsg)
-        .insert();
-
-    return db.get().patchSets().get(newChange.currentPatchSetId());
-  }
-
-  /**
-   * Rebase a commit.
-   *
-   * @param git repository to find commits in.
-   * @param inserter inserter to handle new trees and blobs.
-   * @param original the commit to rebase.
-   * @param base base to rebase against.
-   * @param mergeUtil merge utilities for the destination project.
-   * @param committerIdent committer identity.
-   * @return the id of the rebased commit.
-   * @throws MergeConflictException the rebase failed due to a merge conflict.
-   * @throws IOException the merge failed for another reason.
-   */
-  private ObjectId rebaseCommit(Repository git, ObjectInserter inserter,
-      RevCommit original, RevCommit base, MergeUtil mergeUtil,
-      PersonIdent committerIdent) throws MergeConflictException, IOException,
-      InvalidChangeOperationException {
-    RevCommit parentCommit = original.getParent(0);
-
-    if (base.equals(parentCommit)) {
-      throw new InvalidChangeOperationException(
-          "Change is already up to date.");
-    }
-
-    ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter);
-    merger.setBase(parentCommit);
-    merger.merge(original, base);
-
-    if (merger.getResultTreeId() == null) {
-      throw new MergeConflictException(
-          "The change could not be rebased due to a conflict during merge.");
-    }
-
-    CommitBuilder cb = new CommitBuilder();
-    cb.setTreeId(merger.getResultTreeId());
-    cb.setParentId(base);
-    cb.setAuthor(original.getAuthorIdent());
-    cb.setMessage(original.getFullMessage());
-    cb.setCommitter(committerIdent);
-    ObjectId objectId = inserter.insert(cb);
-    inserter.flush();
-    return objectId;
-  }
-
-  public boolean canRebase(RevisionResource r) {
-    return canRebase(r.getPatchSet(), r.getChange().getDest());
-  }
-
-  private boolean canRebase(PatchSet patchSet, Branch.NameKey dest) {
-    try (Repository git = gitManager.openRepository(dest.getParentKey());
-        RevWalk rw = new RevWalk(git)) {
-      findBaseRevision(patchSet, dest, git, rw);
-      return true;
-    } catch (InvalidChangeOperationException e) {
-      return false;
-    } catch (OrmException | IOException e) {
-      log.warn(String.format(
-          "Error checking if patch set %s on %s can be rebased",
-          patchSet.getId(), dest), e);
-      return false;
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
new file mode 100644
index 0000000..6769a5c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -0,0 +1,208 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.MergeConflictException;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+public class RebaseChangeOp extends BatchUpdate.Op {
+  public interface Factory {
+    RebaseChangeOp create(ChangeControl ctl, PatchSet originalPatchSet,
+        @Nullable String baseCommitish);
+  }
+
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final MergeUtil.Factory mergeUtilFactory;
+
+  private final ChangeControl ctl;
+  private final PatchSet originalPatchSet;
+
+  private String baseCommitish;
+  private PersonIdent committerIdent;
+  private boolean runHooks = true;
+  private CommitValidators.Policy validate;
+  private boolean forceContentMerge;
+
+  private RevCommit rebasedCommit;
+  private PatchSet.Id rebasedPatchSetId;
+  private PatchSetInserter patchSetInserter;
+  private PatchSet rebasedPatchSet;
+
+  @AssistedInject
+  RebaseChangeOp(
+      PatchSetInserter.Factory patchSetInserterFactory,
+      MergeUtil.Factory mergeUtilFactory,
+      @Assisted ChangeControl ctl,
+      @Assisted PatchSet originalPatchSet,
+      @Assisted @Nullable String baseCommitish) {
+    this.patchSetInserterFactory = patchSetInserterFactory;
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.ctl = ctl;
+    this.originalPatchSet = originalPatchSet;
+    this.baseCommitish = baseCommitish;
+  }
+
+  public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
+    this.committerIdent = committerIdent;
+    return this;
+  }
+
+  public RebaseChangeOp setValidatePolicy(CommitValidators.Policy validate) {
+    this.validate = validate;
+    return this;
+  }
+
+  public RebaseChangeOp setRunHooks(boolean runHooks) {
+    this.runHooks = runHooks;
+    return this;
+  }
+
+  public RebaseChangeOp setForceContentMerge(boolean forceContentMerge) {
+    this.forceContentMerge = forceContentMerge;
+    return this;
+  }
+
+  @Override
+  public void updateRepo(RepoContext ctx) throws MergeConflictException,
+       InvalidChangeOperationException, RestApiException, IOException,
+       OrmException {
+    // Ok that originalPatchSet was not read in a transaction, since we just
+    // need its revision.
+    RevId oldRev = originalPatchSet.getRevision();
+
+    RevWalk rw = ctx.getRevWalk();
+    RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get()));
+    rw.parseBody(original);
+    RevCommit baseCommit;
+    if (baseCommitish != null) {
+       baseCommit = rw.parseCommit(ctx.getRepository().resolve(baseCommitish));
+    } else {
+       baseCommit = rw.parseCommit(RebaseUtil.findBaseRevision(
+           originalPatchSet, ctl.getChange().getDest(),
+           ctx.getRepository(), ctx.getRevWalk(), ctx.getDb()));
+    }
+
+    ObjectId newId = rebaseCommit(ctx, original, baseCommit);
+    rebasedCommit = rw.parseCommit(newId);
+
+    rebasedPatchSetId = ChangeUtil.nextPatchSetId(
+        ctx.getRepository(), ctl.getChange().currentPatchSetId());
+    patchSetInserter = patchSetInserterFactory
+        .create(ctl.getRefControl(), rebasedPatchSetId, rebasedCommit)
+        .setDraft(originalPatchSet.isDraft())
+        .setUploader(ctx.getUser().getAccountId())
+        .setSendMail(false)
+        .setRunHooks(runHooks)
+        .setMessage(
+          "Patch Set " + rebasedPatchSetId.get()
+          + ": Patch Set " + originalPatchSet.getId().get() + " was rebased");
+    if (validate != null) {
+      patchSetInserter.setValidatePolicy(validate);
+    }
+    patchSetInserter.updateRepo(ctx);
+  }
+
+  @Override
+  public void updateChange(ChangeContext ctx)
+      throws OrmException, InvalidChangeOperationException {
+    patchSetInserter.updateChange(ctx);
+    rebasedPatchSet = patchSetInserter.getPatchSet();
+  }
+
+  public PatchSet getPatchSet() {
+    checkState(rebasedPatchSet != null,
+        "getPatchSet() only valid after executing update");
+    return rebasedPatchSet;
+  }
+
+  private MergeUtil newMergeUtil() {
+    ProjectState project = ctl.getProjectControl().getProjectState();
+    return forceContentMerge
+        ? mergeUtilFactory.create(project, true)
+        : mergeUtilFactory.create(project);
+  }
+
+  /**
+   * Rebase a commit.
+   *
+   * @param ctx repo context.
+   * @param original the commit to rebase.
+   * @param base base to rebase against.
+   * @return the rebased commit.
+   * @throws MergeConflictException the rebase failed due to a merge conflict.
+   * @throws IOException the merge failed for another reason.
+   */
+  private RevCommit rebaseCommit(RepoContext ctx, RevCommit original,
+      ObjectId base) throws ResourceConflictException, MergeConflictException,
+      IOException {
+    RevCommit parentCommit = original.getParent(0);
+
+    if (base.equals(parentCommit)) {
+      throw new ResourceConflictException("Change is already up to date.");
+    }
+
+    ThreeWayMerger merger = newMergeUtil().newThreeWayMerger(
+        ctx.getRepository(), ctx.getInserter());
+    merger.setBase(parentCommit);
+    merger.merge(original, base);
+
+    if (merger.getResultTreeId() == null) {
+      throw new MergeConflictException(
+          "The change could not be rebased due to a conflict during merge.");
+    }
+
+    CommitBuilder cb = new CommitBuilder();
+    cb.setTreeId(merger.getResultTreeId());
+    cb.setParentId(base);
+    cb.setAuthor(original.getAuthorIdent());
+    cb.setMessage(original.getFullMessage());
+    if (committerIdent != null) {
+      cb.setCommitter(committerIdent);
+    } else {
+      cb.setCommitter(ctx.getUser().asIdentifiedUser()
+          .newRefLogIdent(ctx.getWhen(), ctx.getTimeZone()));
+    }
+    ObjectId objectId = ctx.getInserter().insert(cb);
+    ctx.getInserter().flush();
+    return ctx.getRevWalk().parseCommit(objectId);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
new file mode 100644
index 0000000..aea49f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/** Utility methods related to rebasing changes. */
+public class RebaseUtil {
+  private static final Logger log = LoggerFactory.getLogger(RebaseUtil.class);
+
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager gitManager;
+
+  @Inject
+  RebaseUtil(Provider<ReviewDb> db,
+      GitRepositoryManager gitManager) {
+    this.db = db;
+    this.gitManager = gitManager;
+  }
+
+  public boolean canRebase(RevisionResource r) {
+    PatchSet patchSet = r.getPatchSet();
+    Branch.NameKey dest = r.getChange().getDest();
+    try (Repository git = gitManager.openRepository(dest.getParentKey());
+        RevWalk rw = new RevWalk(git)) {
+      return canRebase(
+          r.getPatchSet(), dest, git, rw, db.get());
+    } catch (IOException e) {
+      log.warn(String.format(
+          "Error checking if patch set %s on %s can be rebased",
+          patchSet.getId(), dest), e);
+      return false;
+    }
+  }
+
+  public static boolean canRebase(PatchSet patchSet, Branch.NameKey dest,
+      Repository git, RevWalk rw, ReviewDb db) {
+    try {
+      findBaseRevision(patchSet, dest, git, rw, db);
+      return true;
+    } catch (RestApiException e) {
+      return false;
+    } catch (OrmException | IOException e) {
+      log.warn(String.format(
+          "Error checking if patch set %s on %s can be rebased",
+          patchSet.getId(), dest), e);
+      return false;
+    }
+  }
+
+  /**
+   * Find the commit onto which a patch set should be rebased.
+   * <p>
+   * This is defined as the latest patch set of the change corresponding to
+   * this commit's parent, or the destination branch tip in the case where the
+   * parent's change is merged.
+   *
+   * @param patchSet patch set for which the new base commit should be found.
+   * @param destBranch the destination branch.
+   * @param git the repository.
+   * @param rw the RevWalk.
+   * @return the commit onto which the patch set should be rebased.
+   * @throws RestApiException if rebase is not possible.
+   * @throws IOException if accessing the repository fails.
+   * @throws OrmException if accessing the database fails.
+   */
+  static ObjectId findBaseRevision(PatchSet patchSet,
+      Branch.NameKey destBranch, Repository git, RevWalk rw, ReviewDb db)
+      throws RestApiException, IOException, OrmException {
+    String baseRev = null;
+    RevCommit commit = rw.parseCommit(
+        ObjectId.fromString(patchSet.getRevision().get()));
+
+    if (commit.getParentCount() > 1) {
+      throw new UnprocessableEntityException(
+          "Cannot rebase a change with multiple parents.");
+    } else if (commit.getParentCount() == 0) {
+      throw new UnprocessableEntityException(
+          "Cannot rebase a change without any parents"
+          + " (is this the initial commit?).");
+    }
+
+    RevId parentRev = new RevId(commit.getParent(0).name());
+
+    for (PatchSet depPatchSet : db.patchSets().byRevision(parentRev)) {
+      Change.Id depChangeId = depPatchSet.getId().getParentKey();
+      Change depChange = db.changes().get(depChangeId);
+      if (!depChange.getDest().equals(destBranch)) {
+        continue;
+      }
+
+      if (depChange.getStatus() == Status.ABANDONED) {
+        throw new ResourceConflictException(
+            "Cannot rebase a change with an abandoned parent: "
+            + depChange.getKey());
+      }
+
+      if (depChange.getStatus().isOpen()) {
+        if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+          throw new ResourceConflictException(
+              "Change is already based on the latest patch set of the"
+              + " dependent change.");
+        }
+        PatchSet latestDepPatchSet =
+            db.patchSets().get(depChange.currentPatchSetId());
+        baseRev = latestDepPatchSet.getRevision().get();
+      }
+      break;
+    }
+
+    if (baseRev == null) {
+      // We are dependent on a merged PatchSet or have no PatchSet
+      // dependencies at all.
+      Ref destRef = git.getRefDatabase().exactRef(destBranch.get());
+      if (destRef == null) {
+        throw new UnprocessableEntityException(
+            "The destination branch does not exist: " + destBranch.get());
+      }
+      baseRev = destRef.getObjectId().getName();
+      if (baseRev.equals(parentRev.get())) {
+        throw new ResourceConflictException("Change is already up to date.");
+      }
+    }
+    return ObjectId.fromString(baseRev);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index c09aa3b..5b0eb6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -16,25 +16,29 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RestoredSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -43,7 +47,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
+import java.util.Collections;
 
 @Singleton
 public class Restore implements RestModifyView<ChangeResource, RestoreInput>,
@@ -54,89 +58,104 @@
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
-  private final ChangeIndexer indexer;
   private final ChangeMessagesUtil cmUtil;
-  private final ChangeUpdate.Factory updateFactory;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
   Restore(ChangeHooks hooks,
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
       ChangeJson.Factory json,
-      ChangeIndexer indexer,
       ChangeMessagesUtil cmUtil,
-      ChangeUpdate.Factory updateFactory) {
+      BatchUpdate.Factory batchUpdateFactory) {
     this.hooks = hooks;
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
-    this.indexer = indexer;
     this.cmUtil = cmUtil;
-    this.updateFactory = updateFactory;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   @Override
   public ChangeInfo apply(ChangeResource req, RestoreInput input)
-      throws AuthException, ResourceConflictException, OrmException,
-      IOException {
-    ChangeControl control = req.getControl();
-    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
-    Change change = req.getChange();
-    if (!control.canRestore()) {
+      throws RestApiException, UpdateException, OrmException {
+    ChangeControl ctl = req.getControl();
+    if (!ctl.canRestore()) {
       throw new AuthException("restore not permitted");
-    } else if (change.getStatus() != Status.ABANDONED) {
-      throw new ResourceConflictException("change is " + status(change));
     }
 
-    ChangeMessage message;
-    ChangeUpdate update;
-    ReviewDb db = dbProvider.get();
-    db.changes().beginTransaction(change.getId());
-    try {
-      change = db.changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus() == Status.ABANDONED) {
-              change.setStatus(Status.NEW);
-              ChangeUtil.updated(change);
-              return change;
-            }
-            return null;
-          }
-        });
-      if (change == null) {
-        throw new ResourceConflictException("change is "
-            + status(db.changes().get(req.getChange().getId())));
+    Op op = new Op(input);
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+      u.addOp(req.getChange().getId(), op).execute();
+    }
+    return json.create(ChangeJson.NO_OPTIONS).format(op.change);
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final RestoreInput input;
+
+    private Change change;
+    private PatchSet patchSet;
+    private ChangeMessage message;
+    private IdentifiedUser caller;
+
+    private Op(RestoreInput input) {
+      this.input = input;
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException,
+        ResourceConflictException {
+      caller = ctx.getUser().asIdentifiedUser();
+      change = ctx.getChange();
+      if (change == null || change.getStatus() != Status.ABANDONED) {
+        throw new ResourceConflictException("change is " + status(change));
+      }
+      patchSet = ctx.getDb().patchSets().get(change.currentPatchSetId());
+      change.setStatus(Status.NEW);
+      change.setLastUpdatedOn(ctx.getWhen());
+      ctx.getDb().changes().update(Collections.singleton(change));
+
+      message = newMessage(ctx.getDb());
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), message);
+    }
+
+    private ChangeMessage newMessage(ReviewDb db) throws OrmException {
+      StringBuilder msg = new StringBuilder();
+      msg.append("Restored");
+      if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
+        msg.append("\n\n");
+        msg.append(input.message.trim());
       }
 
-      //TODO(yyonas): atomic update was not propagated
-      update = updateFactory.create(control);
-      message = newMessage(input, caller, change);
-      cmUtil.addChangeMessage(db, update, message);
-      db.commit();
-    } finally {
-      db.rollback();
+      ChangeMessage message = new ChangeMessage(
+          new ChangeMessage.Key(
+              change.getId(),
+              ChangeUtil.messageUUID(db)),
+          caller.getAccountId(),
+          change.getLastUpdatedOn(),
+          change.currentPatchSetId());
+      message.setMessage(msg.toString());
+      return message;
     }
-    update.commit();
 
-    indexer.index(db, change);
-
-    try {
-      ReplyToChangeSender cm = restoredSenderFactory.create(change.getId());
-      cm.setFrom(caller.getAccountId());
-      cm.setChangeMessage(message);
-      cm.send();
-    } catch (Exception e) {
-      log.error("Cannot email update for change " + change.getChangeId(), e);
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      try {
+        ReplyToChangeSender cm = restoredSenderFactory.create(change.getId());
+        cm.setFrom(caller.getAccountId());
+        cm.setChangeMessage(message);
+        cm.send();
+      } catch (Exception e) {
+        log.error("Cannot email update for change " + change.getId(), e);
+      }
+      hooks.doChangeRestoredHook(change,
+          caller.getAccount(),
+          patchSet,
+          Strings.emptyToNull(input.message),
+          ctx.getDb());
     }
-    hooks.doChangeRestoredHook(change,
-        caller.getAccount(),
-        db.patchSets().get(change.currentPatchSetId()),
-        Strings.emptyToNull(input.message),
-        dbProvider.get());
-    return json.create(ChangeJson.NO_OPTIONS).format(change);
   }
 
   @Override
@@ -148,26 +167,6 @@
           && resource.getControl().canRestore());
   }
 
-  private ChangeMessage newMessage(RestoreInput input, IdentifiedUser caller,
-      Change change) throws OrmException {
-    StringBuilder msg = new StringBuilder();
-    msg.append("Restored");
-    if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
-      msg.append("\n\n");
-      msg.append(input.message.trim());
-    }
-
-    ChangeMessage message = new ChangeMessage(
-        new ChangeMessage.Key(
-            change.getId(),
-            ChangeUtil.messageUUID(dbProvider.get())),
-        caller.getAccountId(),
-        change.getLastUpdatedOn(),
-        change.currentPatchSetId());
-    message.setMessage(msg.toString());
-    return message;
-  }
-
   private static String status(Change change) {
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index aaae6f0..dc2ed5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -16,23 +16,21 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -59,8 +57,8 @@
 
   @Override
   public ChangeInfo apply(ChangeResource req, RevertInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      ResourceNotFoundException, IOException, OrmException, EmailException {
+      throws IOException, OrmException, RestApiException,
+      UpdateException {
     ChangeControl control = req.getControl();
     Change change = req.getChange();
     if (!control.canAddPatchSet()) {
@@ -74,10 +72,7 @@
       revertedChangeId = changeUtil.revert(control,
             change.currentPatchSetId(),
             Strings.emptyToNull(input.message),
-            new PersonIdent(myIdent, TimeUtil.nowTs()),
-            new NoSshInfo());
-    } catch (InvalidChangeOperationException e) {
-      throw new BadRequestException(e.getMessage());
+            new PersonIdent(myIdent, TimeUtil.nowTs()));
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(e.getMessage());
     }
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 29fa0cf..5802c15 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
@@ -110,16 +110,16 @@
 
     List<String> segments = Splitter.on(' ').omitEmptyStrings().splitToList(
         query.toLowerCase());
-    BooleanQuery q = new BooleanQuery();
+    BooleanQuery.Builder q = new BooleanQuery.Builder();
     for (String field : ALL) {
-      BooleanQuery and = new BooleanQuery();
+      BooleanQuery.Builder and = new BooleanQuery.Builder();
       for (String s : segments) {
         and.add(new PrefixQuery(new Term(field, s)), Occur.MUST);
       }
-      q.add(and, Occur.SHOULD);
+      q.add(and.build(), Occur.SHOULD);
     }
 
-    TopDocs results = searcher.search(q, n);
+    TopDocs results = searcher.search(q.build(), n);
     ScoreDoc[] hits = results.scoreDocs;
 
     List<AccountInfo> result = new LinkedList<>();
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 538f36b..6731dd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -84,7 +84,7 @@
   }
 
   IdentifiedUser getUser() {
-    return (IdentifiedUser) getControl().getCurrentUser();
+    return getControl().getUser().asIdentifiedUser();
   }
 
   RevisionResource doNotCache() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
new file mode 100644
index 0000000..61baeb2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.change.HashtagsUtil.extractTags;
+
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.validators.HashtagValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SetHashtagsOp extends BatchUpdate.Op {
+  public interface Factory {
+    SetHashtagsOp create(HashtagsInput input);
+  }
+
+  private final ChangeHooks hooks;
+  private final DynamicSet<HashtagValidationListener> validationListeners;
+  private final HashtagsInput input;
+
+  private boolean runHooks = true;
+
+  private Change change;
+  private Set<String> toAdd;
+  private Set<String> toRemove;
+  private ImmutableSortedSet<String> updatedHashtags;
+
+  @AssistedInject
+  SetHashtagsOp(
+      ChangeHooks hooks,
+      DynamicSet<HashtagValidationListener> validationListeners,
+      @Assisted @Nullable HashtagsInput input) {
+    this.hooks = hooks;
+    this.validationListeners = validationListeners;
+    this.input = input;
+  }
+
+  public SetHashtagsOp setRunHooks(boolean runHooks) {
+    this.runHooks = runHooks;
+    return this;
+  }
+
+  @Override
+  public void updateChange(ChangeContext ctx)
+      throws AuthException, BadRequestException, OrmException, IOException {
+    if (input == null
+        || (input.add == null && input.remove == null)) {
+      updatedHashtags = ImmutableSortedSet.of();
+      return;
+    }
+    if (!ctx.getChangeControl().canEditHashtags()) {
+      throw new AuthException("Editing hashtags not permitted");
+    }
+    ChangeUpdate update = ctx.getChangeUpdate();
+    ChangeNotes notes = update.getChangeNotes().load();
+
+    Set<String> existingHashtags = notes.getHashtags();
+    Set<String> updated = new HashSet<>();
+    toAdd = new HashSet<>(extractTags(input.add));
+    toRemove = new HashSet<>(extractTags(input.remove));
+
+    try {
+      for (HashtagValidationListener validator : validationListeners) {
+        validator.validateHashtags(update.getChange(), toAdd, toRemove);
+      }
+    } catch (ValidationException e) {
+      throw new BadRequestException(e.getMessage());
+    }
+
+    if (existingHashtags != null && !existingHashtags.isEmpty()) {
+      updated.addAll(existingHashtags);
+      toAdd.removeAll(existingHashtags);
+      toRemove.retainAll(existingHashtags);
+    }
+    if (updated()) {
+      updated.addAll(toAdd);
+      updated.removeAll(toRemove);
+      update.setHashtags(updated);
+    }
+
+    change = update.getChange();
+    updatedHashtags = ImmutableSortedSet.copyOf(updated);
+  }
+
+  @Override
+  public void postUpdate(Context ctx) throws OrmException {
+    if (updated() && runHooks) {
+      hooks.doHashtagsChangedHook(
+          change, ctx.getUser().asIdentifiedUser().getAccount(),
+          toAdd, toRemove, updatedHashtags,
+          ctx.getDb());
+    }
+  }
+
+  public ImmutableSortedSet<String> getUpdatedHashtags() {
+    checkState(updatedHashtags != null,
+        "getUpdatedHashtags() only valid after executing op");
+    return updatedHashtags;
+  }
+
+  private boolean updated() {
+    return !isNullOrEmpty(toAdd) || !isNullOrEmpty(toRemove);
+  }
+
+  private static boolean isNullOrEmpty(Collection<?> coll) {
+    return coll == null || coll.isEmpty();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index ec8d70d..1a92ec0 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
@@ -165,7 +165,7 @@
       rsrc = onBehalfOf(rsrc, input);
     }
     ChangeControl control = rsrc.getControl();
-    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
+    IdentifiedUser caller = control.getUser().asIdentifiedUser();
     Change change = rsrc.getChange();
     if (input.onBehalfOf == null && !control.canSubmit()) {
       throw new AuthException("submit not permitted");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
index a4f9fef..7fe738d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -14,15 +14,20 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.WalkSorter.PatchSetData;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,6 +37,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
@@ -43,15 +49,21 @@
 
   private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final MergeSuperSet mergeSuperSet;
+  private final Provider<WalkSorter> sorter;
 
   @Inject
   SubmittedTogether(ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider,
-      MergeSuperSet mergeSuperSet) {
+      Provider<InternalChangeQuery> queryProvider,
+      MergeSuperSet mergeSuperSet,
+      Provider<WalkSorter> sorter) {
     this.json = json;
     this.dbProvider = dbProvider;
+    this.queryProvider = queryProvider;
     this.mergeSuperSet = mergeSuperSet;
+    this.sorter = sorter;
   }
 
   @Override
@@ -59,19 +71,54 @@
       throws AuthException, BadRequestException,
       ResourceConflictException, Exception {
     try {
-      ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(),
-          resource.getChange());
-      if (cs.size() > 1) {
-        return json.create(EnumSet.of(
-            ListChangesOption.CURRENT_REVISION,
-            ListChangesOption.CURRENT_COMMIT))
-          .format(cs.ids());
+      Change c = resource.getChange();
+      List<ChangeData> cds;
+      if (c.getStatus().isOpen()) {
+        cds = getForOpenChange(c);
+      } else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) {
+        cds = getForMergedChange(c);
       } else {
-        return Collections.emptyList();
+        cds = getForAbandonedChange();
       }
+
+      if (cds.size() <= 1) {
+        cds = Collections.emptyList();
+      } else {
+        // Skip sorting for singleton lists, to avoid WalkSorter opening the
+        // repo just to fill out the commit field in PatchSetData.
+        cds = sort(cds);
+      }
+
+      return json.create(EnumSet.of(
+          ListChangesOption.CURRENT_REVISION,
+          ListChangesOption.CURRENT_COMMIT))
+        .formatChangeDatas(cds);
     } catch (OrmException | IOException e) {
       log.error("Error on getting a ChangeSet", e);
       throw e;
     }
   }
+
+  private List<ChangeData> getForOpenChange(Change c)
+      throws OrmException, IOException {
+    ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(), c);
+    return cs.changes().asList();
+  }
+
+  private List<ChangeData> getForMergedChange(Change c) throws OrmException {
+    return queryProvider.get().bySubmissionId(c.getSubmissionId());
+  }
+
+  private List<ChangeData> getForAbandonedChange() {
+    return Collections.emptyList();
+  }
+
+  private List<ChangeData> sort(List<ChangeData> cds)
+      throws OrmException, IOException {
+    List<ChangeData> sorted = new ArrayList<>(cds.size());
+    for (PatchSetData psd : sorter.get().sort(cds)) {
+      sorted.add(psd.data());
+    }
+    return sorted;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index 31e34cf..95a701e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -70,7 +69,7 @@
 
   @Override
   public List<Record> apply(RevisionResource rsrc, Input input)
-      throws AuthException, BadRequestException, OrmException {
+      throws AuthException, OrmException {
     if (input == null) {
       input = new Input();
     }
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 163fb10..6e6ed97 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
@@ -60,7 +60,6 @@
   private final String cookiePath;
   private final boolean cookieSecure;
   private final SignedToken emailReg;
-  private final SignedToken restToken;
 
   @Inject
   AuthConfig(@GerritServerConfig final Config cfg)
@@ -103,15 +102,6 @@
     } else {
       emailReg = null;
     }
-
-    key = cfg.getString("auth", null, "restTokenPrivateKey");
-    if (key != null && !key.isEmpty()) {
-      int age = (int) ConfigUtil.getTimeUnit(cfg,
-          "auth", null, "maxRestTokenAge", 60, TimeUnit.SECONDS);
-      restToken = new SignedToken(age, key);
-    } else {
-      restToken = null;
-    }
   }
 
   private static List<OpenIdProviderPattern> toPatterns(Config cfg, String name) {
@@ -196,10 +186,6 @@
     return emailReg;
   }
 
-  public SignedToken getRestToken() {
-    return restToken;
-  }
-
   /** OpenID identities which the server permits for authentication. */
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index e4d841a..2b6ee41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -14,11 +14,15 @@
 
 package com.google.gerrit.server.config;
 
-import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -56,7 +60,7 @@
 
     String n = valueString.replace(' ', '_').replace('-', '_');
     for (final T e : all) {
-      if (equalsIgnoreCase(e.name(), n)) {
+      if (e.name().equalsIgnoreCase(n)) {
         return e;
       }
     }
@@ -251,9 +255,136 @@
     return v;
   }
 
+  /**
+   * Store section by inspecting Java class attributes.
+   * <p>
+   * Optimize the storage by unsetting a variable if it is
+   * being set to default value by the server.
+   * <p>
+   * Fields marked with final or transient modifiers are skipped.
+   *
+   * @param cfg config in which the values should be stored
+   * @param section section
+   * @param sub subsection
+   * @param s instance of class with config values
+   * @param defaults instance of class with default values
+   * @throws ConfigInvalidException
+   */
+  public static <T> void storeSection(Config cfg, String section, String sub,
+      T s, T defaults) throws ConfigInvalidException {
+    try {
+      for (Field f : s.getClass().getDeclaredFields()) {
+        if (skipField(f)) {
+          continue;
+        }
+        Class<?> t = f.getType();
+        String n = f.getName();
+        f.setAccessible(true);
+        Object c = f.get(s);
+        Object d = f.get(defaults);
+        Preconditions.checkNotNull(d, "Default cannot be null");
+        if (c == null || c.equals(d)) {
+          cfg.unset(section, sub, n);
+        } else {
+          if (isString(t)) {
+            cfg.setString(section, sub, n, (String) c);
+          } else if (isInteger(t)) {
+            cfg.setInt(section, sub, n, (Integer) c);
+          } else if (isLong(t)) {
+            cfg.setLong(section, sub, n, (Long) c);
+          } else if (isBoolean(t)) {
+            cfg.setBoolean(section, sub, n, (Boolean) c);
+          } else if (t.isEnum()) {
+            cfg.setEnum(section, sub, n, (Enum<?>) c);
+          } else {
+            throw new ConfigInvalidException("type is unknown: " + t.getName());
+          }
+        }
+      }
+    } catch (SecurityException | IllegalArgumentException
+        | IllegalAccessException e) {
+      throw new ConfigInvalidException("cannot save values", e);
+    }
+  }
+
+  /**
+   * Load section by inspecting Java class attributes.
+   * <p>
+   * Config values are stored optimized: no default values are stored.
+   * The loading is performed eagerly: all values are set.
+   * <p>
+   * Fields marked with final or transient modifiers are skipped.
+   * <p>
+   * Boolean fields are only set when their values are true.
+   *
+   * @param cfg config from which the values are loaded
+   * @param section section
+   * @param sub subsection
+   * @param s instance of class in which the values are set
+   * @param defaults instance of class with default values
+   * @return loaded instance
+   * @throws ConfigInvalidException
+   */
+  public static <T> T loadSection(Config cfg, String section, String sub,
+      T s, T defaults) throws ConfigInvalidException {
+    try {
+      for (Field f : s.getClass().getDeclaredFields()) {
+        if (skipField(f)) {
+          continue;
+        }
+        Class<?> t = f.getType();
+        String n = f.getName();
+        f.setAccessible(true);
+        Object d = f.get(defaults);
+        Preconditions.checkNotNull(d, "Default cannot be null");
+        if (isString(t)) {
+          f.set(s, MoreObjects.firstNonNull(cfg.getString(section, sub, n), d));
+        } else if (isInteger(t)) {
+          f.set(s, cfg.getInt(section, sub, n, (Integer) d));
+        } else if (isLong(t)) {
+          f.set(s, cfg.getLong(section, sub, n, (Long) d));
+        } else if (isBoolean(t)) {
+          boolean b = cfg.getBoolean(section, sub, n, (Boolean) d);
+          if (b) {
+            f.set(s, b);
+          }
+        } else if (t.isEnum()) {
+          f.set(s, cfg.getEnum(section, sub, n, (Enum<?>) d));
+        } else {
+          throw new ConfigInvalidException("type is unknown: " + t.getName());
+        }
+      }
+    } catch (SecurityException | IllegalArgumentException
+        | IllegalAccessException e) {
+      throw new ConfigInvalidException("cannot load values", e);
+    }
+    return s;
+  }
+
+  private static boolean skipField(Field field) {
+    int modifiers = field.getModifiers();
+    return Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers);
+  }
+
+  private static boolean isString(Class<?> t) {
+    return String.class == t;
+  }
+
+  private static boolean isBoolean(Class<?> t) {
+    return Boolean.class == t || boolean.class == t;
+  }
+
+  private static boolean isLong(Class<?> t) {
+    return Long.class == t || long.class == t;
+  }
+
+  private static boolean isInteger(Class<?> t) {
+    return Integer.class == t || int.class == t;
+  }
+
   private static boolean match(final String a, final String... cases) {
     for (final String b : cases) {
-      if (equalsIgnoreCase(a, b)) {
+      if (b != null && b.equalsIgnoreCase(a)) {
         return true;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
index 789af9d..24d28c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.ConfirmEmail.Input;
@@ -63,10 +62,13 @@
     if (input == null) {
       input = new Input();
     }
+    if (input.token == null) {
+      throw new UnprocessableEntityException("missing token");
+    }
 
     try {
       EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(input.token);
-      Account.Id accId = ((IdentifiedUser)user).getAccountId();
+      Account.Id accId = user.getAccountId();
       if (accId.equals(token.getAccountId())) {
         accountManager.link(accId, token.toAuthRequest());
         return Response.none();
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 3964115..ca3afad 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
@@ -76,6 +76,7 @@
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.EmailMerge;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergeUtil;
@@ -94,8 +95,8 @@
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupModule;
 import com.google.gerrit.server.index.ReindexAfterUpdate;
-import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.AddKeySender;
+import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -188,6 +189,7 @@
     factory(AccountInfoCacheFactory.Factory.class);
     factory(AddReviewerSender.Factory.class);
     factory(AddKeySender.Factory.class);
+    factory(BatchUpdate.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ChangeJson.Factory.class);
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
index b75d775..9eca842 100644
--- 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
@@ -238,6 +238,8 @@
     info.reportBugUrl = cfg.getString("gerrit", null, "reportBugUrl");
     info.reportBugText = cfg.getString("gerrit", null, "reportBugText");
     info.docUrl = getDocUrl(cfg);
+    info.editGpgKeys = toBoolean(enableSignedPush
+        && cfg.getBoolean("gerrit", null, "editGpgKeys", true));
     return info;
   }
 
@@ -367,6 +369,7 @@
     public String docUrl;
     public String reportBugUrl;
     public String reportBugText;
+    public Boolean editGpgKeys;
   }
 
   public static class GitwebInfo {
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 65b0661..382c8fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
@@ -78,7 +78,7 @@
         return BinaryResult.create(Joiner.on('\n').join(cacheNames))
             .base64()
             .setContentType("text/plain")
-            .setCharacterEncoding(UTF_8.name());
+            .setCharacterEncoding(UTF_8);
       } else {
         return cacheNames;
       }
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 44a70d4..5178210 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -68,9 +67,8 @@
   }
 
   @Override
-  public Object apply(ConfigResource rsrc, Input input) throws AuthException,
-      ResourceNotFoundException, BadRequestException,
-      UnprocessableEntityException {
+  public Object apply(ConfigResource rsrc, Input input)
+      throws AuthException, BadRequestException, UnprocessableEntityException {
     if (input == null || input.operation == null) {
       throw new BadRequestException("operation must be specified");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index c705a7d..1d9c795 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.documentation;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.pegdown.Extensions.ALL;
 import static org.pegdown.Extensions.HARDWRAPS;
 import static org.pegdown.Extensions.SUPPRESS_ALL_HTML;
@@ -159,7 +160,7 @@
     try (InputStream in = url.openStream();
         TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024)) {
       tmp.copy(in);
-      return new String(tmp.toByteArray(), "UTF-8");
+      return new String(tmp.toByteArray(), UTF_8);
     }
   }
 }
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 67bdd0b..e44c810 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -123,7 +123,7 @@
       throw new AuthException("Authentication required");
     }
 
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId());
 
     try (Repository repo = gitManager.openRepository(change.getProject())) {
@@ -162,7 +162,7 @@
     }
 
     Change change = edit.getChange();
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     String refName = RefNames.refsEdit(me.getAccountId(), change.getId(),
         current.getId());
     try (Repository repo = gitManager.openRepository(change.getProject());
@@ -227,6 +227,7 @@
   public RefUpdate.Result modifyMessage(ChangeEdit edit, String msg)
       throws AuthException, InvalidChangeOperationException, IOException,
       UnchangedCommitMessageException {
+    msg = msg.trim() + "\n";
     checkState(!Strings.isNullOrEmpty(msg), "message cannot be null");
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
@@ -237,7 +238,7 @@
       throw new UnchangedCommitMessageException();
     }
 
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     Project.NameKey project = edit.getChange().getProject();
     try (Repository repo = gitManager.openRepository(project);
         RevWalk rw = new RevWalk(repo);
@@ -323,7 +324,7 @@
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     Project.NameKey project = edit.getChange().getProject();
     try (Repository repo = gitManager.openRepository(project);
         RevWalk rw = new RevWalk(repo);
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 7682b83..8ac0d55 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
@@ -20,11 +20,11 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -32,11 +32,15 @@
 import com.google.gerrit.server.change.ChangeKind;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -63,27 +67,33 @@
 public class ChangeEditUtil {
   private final GitRepositoryManager gitManager;
   private final PatchSetInserter.Factory patchSetInserterFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ProjectControl.GenericFactory projectControlFactory;
   private final ChangeIndexer indexer;
+  private final ProjectCache projectCache;
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> user;
   private final ChangeKindCache changeKindCache;
+  private final BatchUpdate.Factory updateFactory;
 
   @Inject
   ChangeEditUtil(GitRepositoryManager gitManager,
       PatchSetInserter.Factory patchSetInserterFactory,
-      ChangeControl.GenericFactory changeControlFactory,
+      ProjectControl.GenericFactory projectControlFactory,
       ChangeIndexer indexer,
+      ProjectCache projectCache,
       Provider<ReviewDb> db,
       Provider<CurrentUser> user,
-      ChangeKindCache changeKindCache) {
+      ChangeKindCache changeKindCache,
+      BatchUpdate.Factory updateFactory) {
     this.gitManager = gitManager;
     this.patchSetInserterFactory = patchSetInserterFactory;
-    this.changeControlFactory = changeControlFactory;
+    this.projectControlFactory = projectControlFactory;
     this.indexer = indexer;
+    this.projectCache = projectCache;
     this.db = db;
     this.user = user;
     this.changeKindCache = changeKindCache;
+    this.updateFactory = updateFactory;
   }
 
   /**
@@ -101,7 +111,7 @@
     if (!currentUser.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    return byChange(change, (IdentifiedUser)currentUser);
+    return byChange(change, currentUser.asIdentifiedUser());
   }
 
   /**
@@ -139,13 +149,14 @@
    * its parent.
    *
    * @param edit change edit to publish
-   * @throws NoSuchChangeException
+   * @throws NoSuchProjectException
    * @throws IOException
    * @throws OrmException
-   * @throws ResourceConflictException
+   * @throws UpdateException
+   * @throws RestApiException
    */
-  public void publish(ChangeEdit edit) throws NoSuchChangeException,
-      IOException, OrmException, ResourceConflictException {
+  public void publish(ChangeEdit edit) throws NoSuchProjectException,
+      IOException, OrmException, RestApiException, UpdateException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
         RevWalk rw = new RevWalk(repo);
@@ -156,16 +167,12 @@
             "only edit for current patch set can be published");
       }
 
-      try {
-        Change updatedChange =
-            insertPatchSet(edit, change, repo, rw, basePatchSet,
-                squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
-        // TODO(davido): This should happen in the same BatchRefUpdate.
-        deleteRef(repo, edit);
-        indexer.index(db.get(), updatedChange);
-      } catch (InvalidChangeOperationException e) {
-        throw new ResourceConflictException(e.getMessage());
-      }
+      Change updatedChange =
+          insertPatchSet(edit, change, repo, rw, inserter, basePatchSet,
+              squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
+      // TODO(davido): This should happen in the same BatchRefUpdate.
+      deleteRef(repo, edit);
+      indexer.index(db.get(), updatedChange);
     }
   }
 
@@ -210,20 +217,25 @@
   }
 
   private Change insertPatchSet(ChangeEdit edit, Change change,
-      Repository repo, RevWalk rw, PatchSet basePatchSet, RevCommit squashed)
-      throws NoSuchChangeException, InvalidChangeOperationException,
-      OrmException, IOException {
-    PatchSet ps = new PatchSet(
-        ChangeUtil.nextPatchSetId(change.currentPatchSetId()));
-    ps.setRevision(new RevId(ObjectId.toString(squashed)));
-    ps.setUploader(edit.getUser().getAccountId());
-    ps.setCreatedOn(TimeUtil.nowTs());
+      Repository repo, RevWalk rw, ObjectInserter oi, PatchSet basePatchSet,
+      RevCommit squashed) throws NoSuchProjectException, RestApiException,
+      UpdateException, IOException {
+    RefControl ctl = projectControlFactory
+        .controlFor(change.getProject(), edit.getUser())
+        .controlForRef(change.getDest());
+    PatchSet.Id psId =
+        ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
+    PatchSetInserter inserter =
+        patchSetInserterFactory.create(ctl, psId, squashed);
 
     StringBuilder message = new StringBuilder("Patch set ")
-      .append(ps.getPatchSetId())
+      .append(inserter.getPatchSetId().get())
       .append(": ");
 
-    ChangeKind kind = changeKindCache.getChangeKind(db.get(), change, ps);
+    ProjectState project = projectCache.get(change.getDest().getParentKey());
+    // Previously checked that the base patch set is the current patch set.
+    ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
+    ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed);
     if (kind == ChangeKind.NO_CODE_CHANGE) {
       message.append("Commit message was updated.");
     } else {
@@ -232,15 +244,18 @@
         .append(".");
     }
 
-    PatchSetInserter inserter =
-        patchSetInserterFactory.create(repo, rw,
-            changeControlFactory.controlFor(change, edit.getUser()),
-            squashed);
-    return inserter.setPatchSet(ps)
+    try (BatchUpdate bu = updateFactory.create(
+        db.get(), change.getProject(), ctl.getUser(),
+        TimeUtil.nowTs())) {
+      bu.setRepository(repo, rw, oi);
+      bu.addOp(change.getId(), inserter
         .setDraft(change.getStatus() == Status.DRAFT ||
             basePatchSet.isDraft())
-        .setMessage(message.toString())
-        .insert();
+        .setMessage(message.toString()));
+      bu.execute();
+    }
+
+    return inserter.getChange();
   }
 
   private static void deleteRef(Repository repo, ChangeEdit edit)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
index 7dd8d47..601bcc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -18,7 +18,6 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -34,10 +33,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
 public class UiActions {
   private static final Logger log = LoggerFactory.getLogger(UiActions.class);
 
@@ -50,27 +45,6 @@
     };
   }
 
-  public static List<UiAction.Description> sorted(Iterable<UiAction.Description> in) {
-    List<UiAction.Description> s = Lists.newArrayList(in);
-    Collections.sort(s, new Comparator<UiAction.Description>() {
-      @Override
-      public int compare(UiAction.Description a, UiAction.Description b) {
-        return a.getId().compareTo(b.getId());
-      }
-    });
-    return s;
-  }
-
-  public static Iterable<UiAction.Description> plugins(Iterable<UiAction.Description> in) {
-    return Iterables.filter(in,
-      new Predicate<UiAction.Description>() {
-        @Override
-        public boolean apply(UiAction.Description input) {
-          return input.getId().indexOf('~') > 0;
-        }
-      });
-  }
-
   public static <R extends RestResource> Iterable<UiAction.Description> from(
       RestCollection<?, R> collection,
       R resource,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index eab2785..be17776 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_REJECT_COMMITS;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.reviewdb.client.Project;
@@ -41,7 +42,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
@@ -137,12 +137,12 @@
   }
 
   private ObjectId createNoteContent(String reason, ObjectInserter inserter)
-      throws UnsupportedEncodingException, IOException {
+      throws IOException {
     String noteContent = reason != null ? reason : "";
     if (noteContent.length() > 0 && !noteContent.endsWith("\n")) {
       noteContent = noteContent + "\n";
     }
-    return inserter.insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
+    return inserter.insert(Constants.OBJ_BLOB, noteContent.getBytes(UTF_8));
   }
 
   private PersonIdent createPersonIdent() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
new file mode 100644
index 0000000..c54fe26
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -0,0 +1,389 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Context for a set of updates that should be applied for a site.
+ * <p>
+ * An update operation can be divided into three phases:
+ * <ol>
+ * <li>Git reference updates</li>
+ * <li>Database updates</li>
+ * <li>Post-update steps<li>
+ * </ol>
+ * A single conceptual operation, such as a REST API call or a merge operation,
+ * may make multiple changes at each step, which all need to be serialized
+ * relative to each other. Moreover, for consistency, <em>all</em> git ref
+ * updates must be performed before <em>any</em> database updates, since
+ * database updates might refer to newly-created patch set refs. And all
+ * post-update steps, such as hooks, should run only after all storage
+ * mutations have completed.
+ * <p>
+ * Depending on the backend used, each step might support batching, for example
+ * in a {@code BatchRefUpdate} or one or more database transactions. All
+ * operations in one phase must complete successfully before proceeding to the
+ * next phase.
+ */
+public class BatchUpdate implements AutoCloseable {
+  public interface Factory {
+    public BatchUpdate create(ReviewDb db, Project.NameKey project,
+        CurrentUser user, Timestamp when);
+  }
+
+  public class Context {
+    public Project.NameKey getProject() {
+      return project;
+    }
+
+    public Timestamp getWhen() {
+      return when;
+    }
+
+    public ReviewDb getDb() {
+      return db;
+    }
+
+    public CurrentUser getUser() {
+      return user;
+    }
+  }
+
+  public class RepoContext extends Context {
+    public Repository getRepository() throws IOException {
+      initRepository();
+      return repo;
+    }
+
+    public RevWalk getRevWalk() throws IOException {
+      initRepository();
+      return revWalk;
+    }
+
+    public ObjectInserter getInserter() throws IOException {
+      initRepository();
+      return inserter;
+    }
+
+    public BatchRefUpdate getBatchRefUpdate() throws IOException {
+      initRepository();
+      if (batchRefUpdate == null) {
+        batchRefUpdate = repo.getRefDatabase().newBatchUpdate();
+      }
+      return batchRefUpdate;
+    }
+
+    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
+      getBatchRefUpdate().addCommand(cmd);
+    }
+
+    public TimeZone getTimeZone() {
+      return tz;
+    }
+  }
+
+  public class ChangeContext extends Context {
+    private final ChangeControl ctl;
+    private final ChangeUpdate update;
+
+    private ChangeContext(ChangeControl ctl) {
+      this.ctl = ctl;
+      this.update = changeUpdateFactory.create(ctl, when);
+    }
+
+    public ChangeUpdate getChangeUpdate() {
+      return update;
+    }
+
+    public ChangeNotes getChangeNotes() {
+      return update.getChangeNotes();
+    }
+
+    public ChangeControl getChangeControl() {
+      return ctl;
+    }
+
+    public Change getChange() {
+      return update.getChange();
+    }
+  }
+
+  public static class Op {
+    @SuppressWarnings("unused")
+    public void updateRepo(RepoContext ctx) throws Exception {
+    }
+
+    @SuppressWarnings("unused")
+    public void updateChange(ChangeContext ctx) throws Exception {
+    }
+
+    // TODO(dborowitz): Support async operations?
+    @SuppressWarnings("unused")
+    public void postUpdate(Context ctx) throws Exception {
+    }
+  }
+
+  public abstract static class InsertChangeOp extends Op {
+    public abstract Change getChange();
+  }
+
+  private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
+  private final ChangeIndexer indexer;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ChangeUpdate.Factory changeUpdateFactory;
+  private final GitReferenceUpdated gitRefUpdated;
+
+  private final Project.NameKey project;
+  private final CurrentUser user;
+  private final Timestamp when;
+  private final TimeZone tz;
+
+  private final ListMultimap<Change.Id, Op> ops = ArrayListMultimap.create();
+  private final Map<Change.Id, Change> newChanges = new HashMap<>();
+  private final List<CheckedFuture<?, IOException>> indexFutures =
+      new ArrayList<>();
+
+  private Repository repo;
+  private ObjectInserter inserter;
+  private RevWalk revWalk;
+  private BatchRefUpdate batchRefUpdate;
+  private boolean closeRepo;
+
+  @AssistedInject
+  BatchUpdate(GitRepositoryManager repoManager,
+      ChangeIndexer indexer,
+      ChangeControl.GenericFactory changeControlFactory,
+      ChangeUpdate.Factory changeUpdateFactory,
+      GitReferenceUpdated gitRefUpdated,
+      @GerritPersonIdent PersonIdent serverIdent,
+      @Assisted ReviewDb db,
+      @Assisted Project.NameKey project,
+      @Assisted CurrentUser user,
+      @Assisted Timestamp when) {
+    this.db = db;
+    this.repoManager = repoManager;
+    this.indexer = indexer;
+    this.changeControlFactory = changeControlFactory;
+    this.changeUpdateFactory = changeUpdateFactory;
+    this.gitRefUpdated = gitRefUpdated;
+    this.project = project;
+    this.user = user;
+    this.when = when;
+    tz = serverIdent.getTimeZone();
+  }
+
+  @Override
+  public void close() {
+    if (closeRepo) {
+      revWalk.close();
+      inserter.close();
+      repo.close();
+    }
+  }
+
+  public BatchUpdate setRepository(Repository repo, RevWalk revWalk,
+      ObjectInserter inserter) {
+    checkState(this.repo == null, "repo already set");
+    closeRepo = false;
+    this.repo = checkNotNull(repo, "repo");
+    this.revWalk = checkNotNull(revWalk, "revWalk");
+    this.inserter = checkNotNull(inserter, "inserter");
+    return this;
+  }
+
+  private void initRepository() throws IOException {
+    if (repo == null) {
+      this.repo = repoManager.openRepository(project);
+      closeRepo = true;
+      inserter = repo.newObjectInserter();
+      revWalk = new RevWalk(inserter.newReader());
+    }
+  }
+
+  public CurrentUser getUser() {
+    return user;
+  }
+
+  public Repository getRepository() throws IOException {
+    initRepository();
+    return repo;
+  }
+
+  public RevWalk getRevWalk() throws IOException {
+    initRepository();
+    return revWalk;
+  }
+
+  public ObjectInserter getObjectInserter() throws IOException {
+    initRepository();
+    return inserter;
+  }
+
+  public BatchUpdate addOp(Change.Id id, Op op) {
+    checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
+    ops.put(id, op);
+    return this;
+  }
+
+  public BatchUpdate insertChange(InsertChangeOp op) {
+    Change c = op.getChange();
+    checkArgument(!newChanges.containsKey(c.getId()),
+        "only one op allowed to create change %s", c.getId());
+    newChanges.put(c.getId(), c);
+    ops.get(c.getId()).add(0, op);
+    return this;
+  }
+
+  public void execute() throws UpdateException, RestApiException {
+    try {
+      executeRefUpdates();
+      executeChangeOps();
+      reindexChanges();
+
+      if (batchRefUpdate != null) {
+        // Fire ref update events only after all mutations are finished, since
+        // callers may assume a patch set ref being created means the change was
+        // created, or a branch advancing meaning some changes were closed.
+        gitRefUpdated.fire(project, batchRefUpdate);
+      }
+
+      executePostOps();
+    } catch (UpdateException | RestApiException e) {
+      // Propagate REST API exceptions thrown by operations; they commonly throw
+      // exceptions like ResourceConflictException to indicate an atomic update
+      // failure.
+      throw e;
+    } catch (Exception e) {
+      Throwables.propagateIfPossible(e);
+      throw new UpdateException(e);
+    }
+  }
+
+  private void executeRefUpdates()
+      throws IOException, UpdateException, RestApiException {
+    try {
+      RepoContext ctx = new RepoContext();
+      for (Op op : ops.values()) {
+        op.updateRepo(ctx);
+      }
+    } catch (Exception e) {
+      Throwables.propagateIfPossible(e, RestApiException.class);
+      throw new UpdateException(e);
+    }
+
+    if (repo == null || batchRefUpdate == null
+        || batchRefUpdate.getCommands().isEmpty()) {
+      return;
+    }
+    inserter.flush();
+    batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
+    boolean ok = true;
+    for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
+      if (cmd.getResult() != ReceiveCommand.Result.OK) {
+        ok = false;
+        break;
+      }
+    }
+    if (!ok) {
+      throw new UpdateException("BatchRefUpdate failed: " + batchRefUpdate);
+    }
+  }
+
+  private void executeChangeOps() throws UpdateException, RestApiException {
+    try {
+      for (Map.Entry<Change.Id, Collection<Op>> e : ops.asMap().entrySet()) {
+        Change.Id id = e.getKey();
+        db.changes().beginTransaction(id);
+        ChangeContext ctx;
+        try {
+          ctx = newChangeContext(id);
+          for (Op op : e.getValue()) {
+            op.updateChange(ctx);
+          }
+          db.commit();
+        } finally {
+          db.rollback();
+        }
+        ctx.getChangeUpdate().commit();
+        indexFutures.add(indexer.indexAsync(id));
+      }
+    } catch (Exception e) {
+      Throwables.propagateIfPossible(e, RestApiException.class);
+      throw new UpdateException(e);
+    }
+  }
+
+  private ChangeContext newChangeContext(Change.Id id) throws Exception {
+    Change c = newChanges.get(id);
+    if (c == null) {
+      c = db.changes().get(id);
+    }
+    // Pass in preloaded change to controlFor, to avoid:
+    //  - reading from a db that does not belong to this update
+    //  - attempting to read a change that doesn't exist yet
+    return new ChangeContext(
+      changeControlFactory.controlFor(c, user));
+  }
+
+  private void reindexChanges() throws IOException {
+    ChangeIndexer.allAsList(indexFutures).checkedGet();
+  }
+
+  private void executePostOps() throws Exception {
+    Context ctx = new Context();
+    for (Op op : ops.values()) {
+      op.postUpdate(ctx);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
index d4b0c4b..fdf9b34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
@@ -91,12 +91,12 @@
     return ret;
   }
 
-  public Multimap<Branch.NameKey, Change.Id> changesByBranch()
+  public Multimap<Branch.NameKey, ChangeData> changesByBranch()
       throws OrmException {
-    ListMultimap<Branch.NameKey, Change.Id> ret =
+    ListMultimap<Branch.NameKey, ChangeData> ret =
         ArrayListMultimap.create();
     for (ChangeData cd : changeData) {
-      ret.put(cd.change().getDest(), cd.getId());
+      ret.put(cd.change().getDest(), cd);
     }
     return ret;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 1ff37b0..37a0886 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.base.Function;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.reviewdb.client.Change;
@@ -21,6 +23,8 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -28,6 +32,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
+import java.io.IOException;
 import java.util.List;
 
 /** Extended commit entity with code review specific metadata. */
@@ -50,11 +55,11 @@
         }
       }).nullsFirst();
 
-  public static RevWalk newRevWalk(Repository repo) {
+  public static CodeReviewRevWalk newRevWalk(Repository repo) {
     return new CodeReviewRevWalk(repo);
   }
 
-  public static RevWalk newRevWalk(ObjectReader reader) {
+  public static CodeReviewRevWalk newRevWalk(ObjectReader reader) {
     return new CodeReviewRevWalk(reader);
   }
 
@@ -85,7 +90,7 @@
     return r;
   }
 
-  private static class CodeReviewRevWalk extends RevWalk {
+  public static class CodeReviewRevWalk extends RevWalk {
     private CodeReviewRevWalk(Repository repo) {
       super(repo);
     }
@@ -95,9 +100,42 @@
     }
 
     @Override
-    protected RevCommit createCommit(AnyObjectId id) {
+    protected CodeReviewCommit createCommit(AnyObjectId id) {
       return new CodeReviewCommit(id);
     }
+
+    @Override
+    public CodeReviewCommit next() throws MissingObjectException,
+         IncorrectObjectTypeException, IOException {
+      return (CodeReviewCommit) super.next();
+    }
+
+    @Override
+    public void markStart(RevCommit c) throws MissingObjectException,
+        IncorrectObjectTypeException, IOException {
+      checkArgument(c instanceof CodeReviewCommit);
+      super.markStart(c);
+    }
+
+    @Override
+    public void markUninteresting(final RevCommit c)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      checkArgument(c instanceof CodeReviewCommit);
+      super.markUninteresting(c);
+    }
+
+    @Override
+    public CodeReviewCommit lookupCommit(AnyObjectId id) {
+      return (CodeReviewCommit) super.lookupCommit(id);
+    }
+
+    @Override
+    public CodeReviewCommit parseCommit(AnyObjectId id)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      return (CodeReviewCommit) super.parseCommit(id);
+    }
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 3d3d9b1..66417fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -16,7 +16,7 @@
 
 public enum CommitMergeStatus {
   /** */
-  CLEAN_MERGE("Change has been successfully merged into the git repository"),
+  CLEAN_MERGE("Change has been successfully merged"),
 
   /** */
   CLEAN_PICK("Change has been successfully cherry-picked"),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
index 7b1161c..6535774 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
@@ -96,7 +96,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     throw new OutOfScopeException("No user on email thread");
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/HackPushNegotiateHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/HackPushNegotiateHook.java
new file mode 100644
index 0000000..5cd7dc3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/HackPushNegotiateHook.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+
+import com.google.common.collect.Sets;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.UploadPack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Advertises part of history to git push clients.
+ * <p>
+ * This is a hack to work around the lack of negotiation in the
+ * send-pack/receive-pack wire protocol.
+ * <p>
+ * When the server is frequently advancing master by creating merge commits, the
+ * client may not be able to discover a common ancestor during push. Attempting
+ * to push will re-upload a very large amount of history. This hook hacks in a
+ * fake negotiation replacement by walking history and sending recent commits as
+ * {@code ".have"} lines in the wire protocol, allowing the client to find a
+ * common ancestor.
+ */
+public class HackPushNegotiateHook implements AdvertiseRefsHook {
+  private static final Logger log = LoggerFactory
+      .getLogger(HackPushNegotiateHook.class);
+
+  /** Size of an additional ".have" line. */
+  private static final int HAVE_LINE_LEN = 4
+      + Constants.OBJECT_ID_STRING_LENGTH
+      + 1 + 5 + 1;
+
+  /**
+   * Maximum number of bytes to "waste" in the advertisement with a peek at this
+   * repository's current reachable history.
+   */
+  private static final int MAX_EXTRA_BYTES = 8192;
+
+  /**
+   * Number of recent commits to advertise immediately, hoping to show a client
+   * a nearby merge base.
+   */
+  private static final int BASE_COMMITS = 64;
+
+  /** Number of commits to skip once base has already been shown. */
+  private static final int STEP_COMMITS = 16;
+
+  /** Total number of commits to extract from the history. */
+  private static final int MAX_HISTORY = MAX_EXTRA_BYTES / HAVE_LINE_LEN;
+
+  @Override
+  public void advertiseRefs(UploadPack us) {
+    throw new UnsupportedOperationException(
+        "HackPushNegotiateHook cannot be used for UploadPack");
+  }
+
+  @Override
+  public void advertiseRefs(BaseReceivePack rp)
+      throws ServiceMayNotContinueException {
+    Map<String, Ref> r = rp.getAdvertisedRefs();
+    if (r == null) {
+      try {
+        r = rp.getRepository().getRefDatabase().getRefs(ALL);
+      } catch (ServiceMayNotContinueException e) {
+        throw e;
+      } catch (IOException e) {
+        ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
+        ex.initCause(e);
+        throw ex;
+      }
+    }
+    rp.setAdvertisedRefs(r, history(r.values(), rp));
+  }
+
+  private Set<ObjectId> history(Collection<Ref> refs, BaseReceivePack rp) {
+    Set<ObjectId> alreadySending = rp.getAdvertisedObjects();
+    if (alreadySending.isEmpty()) {
+      alreadySending = idsOf(refs);
+    }
+
+    int max = MAX_HISTORY - Math.max(0, alreadySending.size() - refs.size());
+    if (max <= 0) {
+      return Collections.emptySet();
+    }
+
+    // Scan history until the advertisement is full.
+    RevWalk rw = rp.getRevWalk();
+    try {
+      for (Ref ref : refs) {
+        try {
+          if (ref.getObjectId() != null) {
+            rw.markStart(rw.parseCommit(ref.getObjectId()));
+          }
+        } catch (IOException badCommit) {
+          continue;
+        }
+      }
+
+      Set<ObjectId> history = Sets.newHashSetWithExpectedSize(max);
+      try {
+        int stepCnt = 0;
+        for (RevCommit c; history.size() < max && (c = rw.next()) != null;) {
+          if (c.getParentCount() <= 1
+              && !alreadySending.contains(c)
+              && (history.size() < BASE_COMMITS || (++stepCnt % STEP_COMMITS) == 0)) {
+            history.add(c);
+          }
+        }
+      } catch (IOException err) {
+        log.error("error trying to advertise history", err);
+      }
+      return history;
+    } finally {
+      rw.reset();
+    }
+  }
+
+  private static Set<ObjectId> idsOf(Collection<Ref> refs) {
+    Set<ObjectId> r = Sets.newHashSetWithExpectedSize(refs.size());
+    for (Ref ref : refs) {
+      if (ref.getObjectId() != null) {
+        r.add(ref.getObjectId());
+      }
+    }
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 624e30c..eebae70 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
@@ -201,7 +201,7 @@
   public Repository createRepository(Project.NameKey name)
       throws RepositoryNotFoundException, RepositoryCaseMismatchException {
     Repository repo = createRepository(basePath, name);
-    if (noteDbPath != null) {
+    if (noteDbPath != null && !noteDbPath.equals(basePath)) {
       createRepository(noteDbPath, name);
     }
     return repo;
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 3bfa47e..776cee9 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
@@ -27,6 +27,8 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Table;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -50,6 +52,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
@@ -86,11 +89,12 @@
 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.net.InetAddress;
+import java.net.UnknownHostException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -143,12 +147,24 @@
 
   private final Map<Change.Id, List<SubmitRecord>> records;
   private final Map<Change.Id, CodeReviewCommit> commits;
-  private String logPrefix;
+
+  private static final String MACHINE_ID;
+  static {
+    String id;
+    try {
+      id = InetAddress.getLocalHost().getHostAddress();
+    } catch (UnknownHostException e) {
+      id = "unknown";
+    }
+    MACHINE_ID = id;
+  }
+  private String staticSubmissionId;
+  private String submissionId;
 
   private ProjectState destProject;
   private ReviewDb db;
   private Repository repo;
-  private RevWalk rw;
+  private CodeReviewRevWalk rw;
   private RevFlag canMergeFlag;
   private ObjectInserter inserter;
   private PersonIdent refLogIdent;
@@ -229,6 +245,10 @@
   public static List<SubmitRecord> checkSubmitRule(ChangeData cd)
       throws ResourceConflictException, OrmException {
     PatchSet patchSet = cd.currentPatchSet();
+    if (patchSet == null) {
+      throw new ResourceConflictException(
+          "missing current patch set for change " + cd.getId());
+    }
     List<SubmitRecord> results = new SubmitRuleEvaluator(cd)
         .setPatchSet(patchSet)
         .evaluate();
@@ -332,10 +352,19 @@
     }
   }
 
+  private void updateSubmissionId(Change change) {
+    Hasher h = Hashing.sha1().newHasher();
+    h.putLong(Thread.currentThread().getId())
+        .putUnencodedChars(MACHINE_ID);
+    staticSubmissionId = h.hash().toString().substring(0, 8);
+    submissionId = change.getId().get() + "-" + TimeUtil.nowMs() +
+        "-" + staticSubmissionId;
+  }
+
   public void merge(ReviewDb db, Change change, IdentifiedUser caller,
       boolean checkSubmitRules) throws NoSuchChangeException,
       OrmException, ResourceConflictException {
-    logPrefix = String.format("[%s]: ", String.valueOf(change.hashCode()));
+    updateSubmissionId(change);
     this.db = db;
     logDebug("Beginning integration of {}", change);
     try {
@@ -365,18 +394,14 @@
     logDebug("Perform the merges");
     try {
       Multimap<Project.NameKey, Branch.NameKey> br = cs.branchesByProject();
-      Multimap<Branch.NameKey, Change.Id> cbb = cs.changesByBranch();
+      Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
       for (Project.NameKey project : br.keySet()) {
         openRepository(project);
         for (Branch.NameKey branch : br.get(project)) {
           setDestProject(branch);
 
-          List<ChangeData> cds = new ArrayList<>();
-          for (Change.Id id : cbb.get(branch)) {
-            cds.add(changeDataFactory.create(db, id));
-          }
           ListMultimap<SubmitType, ChangeData> submitting =
-              validateChangeList(cds);
+              validateChangeList(cbb.get(branch));
           toSubmit.put(branch, submitting);
 
           Set<SubmitType> submitTypes = new HashSet<>(submitting.keySet());
@@ -473,6 +498,7 @@
     rw = CodeReviewCommit.newRevWalk(repo);
     rw.sort(RevSort.TOPO);
     rw.sort(RevSort.COMMIT_TIME_DESC, true);
+    rw.setRetainBody(false);
     canMergeFlag = rw.newFlag("CAN_MERGE");
 
     inserter = repo.newObjectInserter();
@@ -503,8 +529,7 @@
       RefUpdate branchUpdate = repo.updateRef(destBranch.get());
       CodeReviewCommit branchTip;
       if (branchUpdate.getOldObjectId() != null) {
-        branchTip =
-            (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
+        branchTip = rw.parseCommit(branchUpdate.getOldObjectId());
       } else if (Objects.equals(repo.getFullBranch(), destBranch.get())) {
         branchTip = null;
         branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
@@ -558,7 +583,7 @@
   }
 
   private ListMultimap<SubmitType, ChangeData> validateChangeList(
-      List<ChangeData> submitted) throws MergeException {
+      Collection<ChangeData> submitted) throws MergeException {
     logDebug("Validating {} changes", submitted.size());
     ListMultimap<SubmitType, ChangeData> toSubmit = ArrayListMultimap.create();
 
@@ -637,7 +662,7 @@
 
       CodeReviewCommit commit;
       try {
-        commit = (CodeReviewCommit) rw.parseCommit(id);
+        commit = rw.parseCommit(id);
       } catch (IOException e) {
         logError("Invalid commit " + idstr + " on patch set " + ps.getId(), e);
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
@@ -994,6 +1019,7 @@
       @Override
       public Change update(Change c) {
         c.setStatus(Change.Status.MERGED);
+        c.setSubmissionId(submissionId);
         if (!merged.equals(c.currentPatchSetId())) {
           // Uncool; the patch set changed after we merged it.
           // Go back to the patch set that was actually merged.
@@ -1250,28 +1276,28 @@
 
   private void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
-      log.debug(logPrefix + msg, args);
+      log.debug("[" + submissionId + "]" + msg, args);
     }
   }
 
   private void logWarn(String msg, Throwable t) {
     if (log.isWarnEnabled()) {
-      log.warn(logPrefix + msg, t);
+      log.warn("[" + submissionId + "]" + msg, t);
     }
   }
 
   private void logWarn(String msg) {
     if (log.isWarnEnabled()) {
-      log.warn(logPrefix + msg);
+      log.warn("[" + submissionId + "]" + msg);
     }
   }
 
   private void logError(String msg, Throwable t) {
     if (log.isErrorEnabled()) {
       if (t != null) {
-        log.error(logPrefix + msg, t);
+        log.error("[" + submissionId + "]" + msg, t);
       } else {
-        log.error(logPrefix + msg);
+        log.error("[" + submissionId + "]" + msg);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
index 4985390..aa55751 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevCommitList;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -27,12 +28,12 @@
 import java.util.Set;
 
 public class MergeSorter {
-  private final RevWalk rw;
+  private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final Set<RevCommit> accepted;
 
-  public MergeSorter(final RevWalk rw, final Set<RevCommit> alreadyAccepted,
-      final RevFlag canMergeFlag) {
+  public MergeSorter(CodeReviewRevWalk rw, Set<RevCommit> alreadyAccepted,
+      RevFlag canMergeFlag) {
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.accepted = alreadyAccepted;
@@ -51,8 +52,8 @@
         rw.markUninteresting(c);
       }
 
-      RevCommit c;
-      final RevCommitList<RevCommit> contents = new RevCommitList<>();
+      CodeReviewCommit c;
+      RevCommitList<RevCommit> contents = new RevCommitList<>();
       while ((c = rw.next()) != null) {
         if (!c.has(canMergeFlag) || !incoming.contains(c)) {
           // We cannot merge n as it would bring something we
@@ -62,7 +63,7 @@
             n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
             n.missing = new ArrayList<>();
           }
-          n.missing.add((CodeReviewCommit) c);
+          n.missing.add(c);
         } else {
           contents.add(c);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index 3add138..c9a5c3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -139,7 +139,8 @@
           if (!hashes.isEmpty()) {
             // Merged changes are ok to exclude
             Iterable<ChangeData> destChanges = queryProvider.get()
-                .byCommitsOnBranchNotMerged(cd.change().getDest(), hashes);
+                .byCommitsOnBranchNotMerged(
+                  repo, db, cd.change().getDest(), hashes);
             for (ChangeData chd : destChanges) {
               ret.add(chd);
             }
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 c5ead54..5fdefbe 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
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
@@ -164,9 +165,10 @@
     return result;
   }
 
-  public RevCommit createCherryPickFromCommit(Repository repo,
+  public CodeReviewCommit createCherryPickFromCommit(Repository repo,
       ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
-      PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
+      PersonIdent cherryPickCommitterIdent, String commitMsg,
+      CodeReviewRevWalk rw)
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
       MergeIdenticalTreeException, MergeConflictException {
 
@@ -361,9 +363,9 @@
     }
   }
 
-  public boolean canFastForward(final MergeSorter mergeSorter,
-      final CodeReviewCommit mergeTip, final RevWalk rw,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean canFastForward(MergeSorter mergeSorter,
+      CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge)
+      throws MergeException {
     if (hasMissingDependencies(mergeSorter, toMerge)) {
       return false;
     }
@@ -375,9 +377,9 @@
     }
   }
 
-  public boolean canCherryPick(final MergeSorter mergeSorter,
-      final Repository repo, final CodeReviewCommit mergeTip, final RevWalk rw,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean canCherryPick(MergeSorter mergeSorter, Repository repo,
+      CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge)
+      throws MergeException {
     if (mergeTip == null) {
       // The branch is unborn. Fast-forward is possible.
       //
@@ -445,7 +447,7 @@
   }
 
   public CodeReviewCommit mergeOneCommit(PersonIdent author,
-      PersonIdent committer, Repository repo, RevWalk rw,
+      PersonIdent committer, Repository repo, CodeReviewRevWalk rw,
       ObjectInserter inserter, RevFlag canMergeFlag, Branch.NameKey destBranch,
       CodeReviewCommit mergeTip, CodeReviewCommit n) throws MergeException {
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
@@ -481,22 +483,22 @@
     }
   }
 
-  private static CodeReviewCommit failed(final RevWalk rw,
-      final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
-      final CodeReviewCommit n, final CommitMergeStatus failure)
+  private static CodeReviewCommit failed(CodeReviewRevWalk rw,
+      RevFlag canMergeFlag, CodeReviewCommit mergeTip, CodeReviewCommit n,
+      CommitMergeStatus failure)
       throws MissingObjectException, IncorrectObjectTypeException, IOException {
     rw.resetRetain(canMergeFlag);
     rw.markStart(n);
     rw.markUninteresting(mergeTip);
     CodeReviewCommit failed;
-    while ((failed = (CodeReviewCommit) rw.next()) != null) {
+    while ((failed = rw.next()) != null) {
       failed.setStatusCode(failure);
     }
     return failed;
   }
 
   public CodeReviewCommit writeMergeCommit(PersonIdent author,
-      PersonIdent committer, RevWalk rw, ObjectInserter inserter,
+      PersonIdent committer, CodeReviewRevWalk rw, ObjectInserter inserter,
       RevFlag canMergeFlag, Branch.NameKey destBranch,
       CodeReviewCommit mergeTip, ObjectId treeId, CodeReviewCommit n)
       throws IOException, MissingObjectException,
@@ -505,8 +507,8 @@
     rw.resetRetain(canMergeFlag);
     rw.markStart(n);
     rw.markUninteresting(mergeTip);
-    for (final RevCommit c : rw) {
-      final CodeReviewCommit crc = (CodeReviewCommit) c;
+    CodeReviewCommit crc;
+    while ((crc = rw.next()) != null) {
       if (crc.getPatchsetId() != null) {
         merged.add(crc);
       }
@@ -536,7 +538,7 @@
     mergeCommit.setMessage(msgbuf.toString());
 
     CodeReviewCommit mergeResult =
-        (CodeReviewCommit) rw.parseCommit(commit(inserter, mergeCommit));
+        rw.parseCommit(commit(inserter, mergeCommit));
     mergeResult.setControl(n.getControl());
     return mergeResult;
   }
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 b868282..addbc1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -118,6 +118,7 @@
       "requireContributorAgreement";
   private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";
   private static final String KEY_ENABLE_SIGNED_PUSH = "enableSignedPush";
+  private static final String KEY_REQUIRE_SIGNED_PUSH = "requireSignedPush";
 
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
@@ -420,6 +421,8 @@
     p.setCreateNewChangeForAllNotInTarget(getEnum(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, InheritableBoolean.INHERIT));
     p.setEnableSignedPush(getEnum(rc, RECEIVE, null,
           KEY_ENABLE_SIGNED_PUSH, InheritableBoolean.INHERIT));
+    p.setRequireSignedPush(getEnum(rc, RECEIVE, null,
+          KEY_REQUIRE_SIGNED_PUSH, InheritableBoolean.INHERIT));
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
@@ -547,7 +550,7 @@
       Config rc, Map<String, GroupReference> groupsByName) {
     accessSections = new HashMap<>();
     for (String refName : rc.getSubsections(ACCESS)) {
-      if (RefConfigSection.isValid(refName)) {
+      if (RefConfigSection.isValid(refName) & isValidRegex(refName)) {
         AccessSection as = getAccessSection(refName, true);
 
         for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
@@ -580,6 +583,17 @@
     }
   }
 
+  private boolean isValidRegex(String refPattern) {
+    try {
+      Pattern.compile(refPattern.replace("${username}/", ""));
+    } catch (PatternSyntaxException e) {
+      error(new ValidationError(PROJECT_CONFIG, "Invalid ref name: "
+          + e.getMessage()));
+      return false;
+    }
+    return true;
+  }
+
   private void loadBranchOrderSection(Config rc) {
     if (rc.getSections().contains(BRANCH_ORDER)) {
       branchOrderSection = new BranchOrderSection(
@@ -828,6 +842,8 @@
     set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
     set(rc, RECEIVE, null, KEY_ENABLE_SIGNED_PUSH,
         p.getEnableSignedPush(), InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_PUSH,
+        p.getRequireSignedPush(), InheritableBoolean.INHERIT);
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
     set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
index 8595582..3ee2a9b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -28,13 +29,12 @@
 import java.util.Set;
 
 public class RebaseSorter {
-
-  private final RevWalk rw;
+  private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final Set<RevCommit> accepted;
 
-  public RebaseSorter(final RevWalk rw, final Set<RevCommit> alreadyAccepted,
-      final RevFlag canMergeFlag) {
+  public RebaseSorter(CodeReviewRevWalk rw, Set<RevCommit> alreadyAccepted,
+      RevFlag canMergeFlag) {
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.accepted = alreadyAccepted;
@@ -55,7 +55,7 @@
 
       CodeReviewCommit c;
       final List<CodeReviewCommit> contents = new ArrayList<>();
-      while ((c = (CodeReviewCommit) rw.next()) != null) {
+      while ((c = rw.next()) != null) {
         if (!c.has(canMergeFlag) || !incoming.contains(c)) {
           // We cannot merge n as it would bring something we
           // aren't permitted to merge at this time. Drop n.
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 9e5353a..c87ed00 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
@@ -62,6 +62,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -71,6 +72,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -89,6 +91,7 @@
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.SetHashtagsOp;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -136,6 +139,7 @@
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -273,7 +277,7 @@
   private Set<Account.Id> reviewersFromCommandLine = Sets.newLinkedHashSet();
   private Set<Account.Id> ccFromCommandLine = Sets.newLinkedHashSet();
 
-  private final IdentifiedUser currentUser;
+  private final IdentifiedUser user;
   private final ReviewDb db;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeData.Factory changeDataFactory;
@@ -305,6 +309,8 @@
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
   private final ChangeKindCache changeKindCache;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final SetHashtagsOp.Factory hashtagsFactory;
 
   private final ProjectControl projectControl;
   private final Project project;
@@ -381,8 +387,10 @@
       final ChangeKindCache changeKindCache,
       final DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       final NotesMigration notesMigration,
-      final ChangeEditUtil editUtil) throws IOException {
-    this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
+      final ChangeEditUtil editUtil,
+      final BatchUpdate.Factory batchUpdateFactory,
+      final SetHashtagsOp.Factory hashtagsFactory) throws IOException {
+    this.user = projectControl.getUser().asIdentifiedUser();
     this.db = db;
     this.queryProvider = queryProvider;
     this.changeDataFactory = changeDataFactory;
@@ -414,6 +422,8 @@
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
     this.changeKindCache = changeKindCache;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.hashtagsFactory = hashtagsFactory;
 
     this.projectControl = projectControl;
     this.labelTypes = projectControl.getLabelTypes();
@@ -485,6 +495,7 @@
     advHooks.add(rp.getAdvertiseRefsHook());
     advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
         db, queryProvider, projectControl.getProject().getNameKey()));
+    advHooks.add(new HackPushNegotiateHook());
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
   }
 
@@ -595,7 +606,7 @@
       for (Error error : errors.keySet()) {
         rp.sendMessage(buildError(error, errors.get(error)));
       }
-      rp.sendMessage(String.format("User: %s", displayName(currentUser)));
+      rp.sendMessage(String.format("User: %s", displayName(user)));
       rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
     }
 
@@ -651,7 +662,7 @@
                 new Branch.NameKey(project.getNameKey(), c.getRefName()),
                 c.getOldId(),
                 c.getNewId(),
-                currentUser.getAccount());
+                user.getAccount());
           }
         }
     }
@@ -870,7 +881,7 @@
       }
 
       HookResult result = hooks.doRefUpdateHook(project, cmd.getRefName(),
-                              currentUser.getAccount(), cmd.getOldId(),
+                              user.getAccount(), cmd.getOldId(),
                               cmd.getNewId());
 
       if (result != null) {
@@ -941,7 +952,7 @@
                   addError("  " + err.getMessage());
                 }
                 reject(cmd, "invalid project configuration");
-                log.error("User " + currentUser.getUserName()
+                log.error("User " + user.getUserName()
                     + " tried to push invalid project configuration "
                     + cmd.getNewId().name() + " for " + project.getName());
                 continue;
@@ -956,7 +967,7 @@
                 }
               } else {
                 if (!oldParent.equals(newParent)
-                    && !currentUser.getCapabilities().canAdministrateServer()) {
+                    && !user.getCapabilities().canAdministrateServer()) {
                   reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
                   continue;
                 }
@@ -1002,7 +1013,7 @@
               }
             } catch (Exception e) {
               reject(cmd, "invalid project configuration");
-              log.error("User " + currentUser.getUserName()
+              log.error("User " + user.getUserName()
                   + " tried to push invalid project configuration "
                   + cmd.getNewId().name() + " for " + project.getName(), e);
               continue;
@@ -1518,7 +1529,7 @@
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<>();
       final int maxBatchChanges =
-          receiveConfig.getEffectiveMaxBatchChangesLimit(currentUser);
+          receiveConfig.getEffectiveMaxBatchChangesLimit(user);
       for (;;) {
         final RevCommit c = rp.getRevWalk().next();
         if (c == null) {
@@ -1712,12 +1723,14 @@
       commit = c;
       change = new Change(changeKey,
           new Change.Id(db.nextChangeId()),
-          currentUser.getAccountId(),
+          user.getAccountId(),
           magicBranch.dest,
           TimeUtil.nowTs());
       change.setTopic(magicBranch.topic);
-      ins = changeInserterFactory.create(ctl.getProjectControl(), change, c)
-          .setDraft(magicBranch.draft);
+      ins = changeInserterFactory.create(ctl, change, c)
+          .setDraft(magicBranch.draft)
+          // Changes already validated in validateNewCommits.
+          .setValidatePolicy(CommitValidators.Policy.NONE);
       cmd = new ReceiveCommand(ObjectId.zeroId(), c,
           ins.getPatchSet().getRefName());
     }
@@ -1729,13 +1742,13 @@
       ListenableFuture<Void> future = changeUpdateExector.submit(
           requestScopePropagator.wrap(new Callable<Void>() {
         @Override
-        public Void call() throws OrmException, IOException,
-            ResourceConflictException {
+        public Void call()
+            throws OrmException, RestApiException, UpdateException {
           if (caller == Thread.currentThread()) {
-            insertChange(db);
+            insertChange(ReceiveCommits.this.db);
           } else {
-            try (ReviewDb db = schemaFactory.open()) {
-              insertChange(db);
+            try (ReviewDb threadLocalDb = schemaFactory.open()) {
+              insertChange(threadLocalDb);
             }
           }
           synchronized (newProgress) {
@@ -1747,34 +1760,41 @@
       return Futures.makeChecked(future, INSERT_EXCEPTION);
     }
 
-    private void insertChange(ReviewDb db) throws OrmException, IOException,
-        ResourceConflictException {
+    private void insertChange(ReviewDb threadLocalDb)
+        throws OrmException, RestApiException, UpdateException {
       final PatchSet ps = ins.setGroups(groups).getPatchSet();
-      final Account.Id me = currentUser.getAccountId();
+      final Account.Id me = user.getAccountId();
       final List<FooterLine> footerLines = commit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
       Map<String, Short> approvals = new HashMap<>();
       if (magicBranch != null) {
         recipients.add(magicBranch.getMailRecipients());
         approvals = magicBranch.labels;
-        ins.setHashtags(magicBranch.hashtags);
       }
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
-
-      ChangeMessage msg =
-          new ChangeMessage(new ChangeMessage.Key(change.getId(),
-              ChangeUtil.messageUUID(db)), me, ps.getCreatedOn(), ps.getId());
-      msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + ".");
-
-      ins
-        .setReviewers(recipients.getReviewers())
-        .setExtraCC(recipients.getCcOnly())
-        .setApprovals(approvals)
-        .setMessage(msg)
-        .setRequestScopePropagator(requestScopePropagator)
-        .setSendMail(true)
-        .insert();
+      String msg = renderMessageWithApprovals(ps.getPatchSetId(), null,
+          approvals, Collections.<String, PatchSetApproval> emptyMap());
+      try (ObjectInserter oi = repo.newObjectInserter();
+          BatchUpdate bu = batchUpdateFactory.create(threadLocalDb,
+            change.getProject(), user, change.getCreatedOn())) {
+        bu.setRepository(repo, rp.getRevWalk(), oi);
+        bu.insertChange(ins
+            .setReviewers(recipients.getReviewers())
+            .setExtraCC(recipients.getCcOnly())
+            .setApprovals(approvals)
+            .setMessage(msg)
+            .setRequestScopePropagator(requestScopePropagator)
+            .setSendMail(true)
+            .setUpdateRef(false));
+        if (magicBranch != null) {
+          bu.addOp(
+              ins.getChange().getId(),
+              hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags))
+                .setRunHooks(false));
+        }
+        bu.execute();
+      }
       created = true;
 
       if (magicBranch != null && magicBranch.submit) {
@@ -1789,7 +1809,7 @@
     RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
     try {
       mergeOpProvider.get().merge(db, rsrc.getChange(),
-          (IdentifiedUser) changeCtl.getCurrentUser(), false);
+          changeCtl.getUser().asIdentifiedUser(), false);
     } catch (NoSuchChangeException e) {
       throw new OrmException(e);
     }
@@ -1881,6 +1901,32 @@
     }
   }
 
+  private String renderMessageWithApprovals(int patchSetId, String suffix,
+      Map<String, Short> n, Map<String, PatchSetApproval> c) {
+    StringBuilder msgs = new StringBuilder("Uploaded patch set " + patchSetId);
+    if (!n.isEmpty()) {
+      boolean first = true;
+      for (Map.Entry<String, Short> e : n.entrySet()) {
+        if (c.containsKey(e.getKey())
+            && c.get(e.getKey()).getValue() == e.getValue()) {
+          continue;
+        }
+        if (first) {
+          msgs.append(":");
+          first = false;
+        }
+        msgs.append(" ")
+            .append(LabelVote.create(e.getKey(), e.getValue()).format());
+      }
+    }
+
+    if (!Strings.isNullOrEmpty(suffix)) {
+      msgs.append(suffix);
+    }
+
+    return msgs.append('.').toString();
+  }
+
   private class ReplaceRequest {
     final Change.Id ontoChange;
     final RevCommit newCommit;
@@ -2027,7 +2073,7 @@
       Optional<ChangeEdit> edit = null;
 
       try {
-        edit = editUtil.byChange(change, currentUser);
+        edit = editUtil.byChange(change, user);
       } catch (IOException e) {
         log.error("Cannt retrieve edit", e);
         return false;
@@ -2061,23 +2107,28 @@
           ObjectId.zeroId(),
           newCommit,
           RefNames.refsEdit(
-              currentUser.getAccountId(),
+              user.getAccountId(),
               change.getId(),
               newPatchSet.getId()));
     }
 
-    private void newPatchSet() {
+    private void newPatchSet() throws IOException {
       PatchSet.Id id =
           ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId());
       newPatchSet = new PatchSet(id);
       newPatchSet.setCreatedOn(TimeUtil.nowTs());
-      newPatchSet.setUploader(currentUser.getAccountId());
+      newPatchSet.setUploader(user.getAccountId());
       newPatchSet.setRevision(toRevId(newCommit));
       newPatchSet.setGroups(groups);
+      if (rp.getPushCertificate() != null) {
+        newPatchSet.setPushCertificate(
+            rp.getPushCertificate().toTextWithSignature());
+      }
       if (magicBranch != null && magicBranch.draft) {
         newPatchSet.setDraft(true);
       }
-      info = patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+      info = patchSetInfoFactory.get(
+          rp.getRevWalk(), newCommit, newPatchSet.getId());
       cmd = new ReceiveCommand(
           ObjectId.zeroId(),
           newCommit,
@@ -2114,27 +2165,52 @@
       return Futures.makeChecked(future, INSERT_EXCEPTION);
     }
 
-    private ChangeMessage newChangeMessage(ReviewDb db, ChangeKind changeKind)
+    private ChangeMessage newChangeMessage(ReviewDb db, ChangeKind changeKind,
+        Map<String, Short> approvals)
         throws OrmException {
       msg =
           new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-              .messageUUID(db)), currentUser.getAccountId(), newPatchSet.getCreatedOn(),
+              .messageUUID(db)), user.getAccountId(), newPatchSet.getCreatedOn(),
               newPatchSet.getId());
-      String message = "Uploaded patch set " + newPatchSet.getPatchSetId();
+
+      msg.setMessage(renderMessageWithApprovals(newPatchSet.getPatchSetId(),
+          changeKindMessage(changeKind), approvals, scanLabels(db, approvals)));
+
+      return msg;
+    }
+
+    private String changeKindMessage(ChangeKind changeKind) {
       switch (changeKind) {
         case TRIVIAL_REBASE:
         case NO_CHANGE:
-          message += ": Patch Set " + priorPatchSet.get() + " was rebased";
-          break;
+          return ": Patch Set " + priorPatchSet.get() + " was rebased";
         case NO_CODE_CHANGE:
-          message += ": Commit message was updated";
-          break;
+          return ": Commit message was updated";
         case REWORK:
         default:
-          break;
+          return null;
       }
-      msg.setMessage(message + ".");
-      return msg;
+    }
+
+    private Map<String, PatchSetApproval> scanLabels(ReviewDb db,
+        Map<String, Short> approvals)
+        throws OrmException {
+      Map<String, PatchSetApproval> current = new HashMap<>();
+      // We optimize here and only retrieve current when approvals provided
+      if (!approvals.isEmpty()) {
+        for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
+            db, changeCtl, priorPatchSet, user.getAccountId())) {
+          if (a.isSubmit()) {
+            continue;
+          }
+
+          LabelType lt = labelTypes.byLabel(a.getLabelId());
+          if (lt != null) {
+            current.put(lt.getName(), a);
+          }
+        }
+      }
+      return current;
     }
 
     PatchSet.Id upsertEdit() {
@@ -2146,7 +2222,7 @@
 
     PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException,
         ResourceConflictException {
-      final Account.Id me = currentUser.getAccountId();
+      final Account.Id me = user.getAccountId();
       final List<FooterLine> footerLines = newCommit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
       Map<String, Short> approvals = new HashMap<>();
@@ -2193,7 +2269,7 @@
         approvalCopier.copy(db, changeCtl, newPatchSet);
         approvalsUtil.addReviewers(db, update, labelTypes, change, newPatchSet,
             info, recipients.getReviewers(), oldRecipients.getAll());
-        approvalsUtil.addApprovals(db, update, labelTypes, newPatchSet, info,
+        approvalsUtil.addApprovals(db, update, labelTypes, newPatchSet,
             changeCtl, approvals);
         recipients.add(oldRecipients);
 
@@ -2201,7 +2277,8 @@
         changeKind = changeKindCache.getChangeKind(
             projectControl.getProjectState(), repo, priorCommit, newCommit);
 
-        cmUtil.addChangeMessage(db, update, newChangeMessage(db, changeKind));
+        cmUtil.addChangeMessage(db, update, newChangeMessage(db, changeKind,
+            approvals));
 
         if (mergedIntoRef == null) {
           // Change should be new, so it can go through review again.
@@ -2297,7 +2374,12 @@
       hooks.doPatchsetCreatedHook(change, newPatchSet, db);
       if (mergedIntoRef != null) {
         hooks.doChangeMergedHook(
-            change, currentUser.getAccount(), newPatchSet, db, newCommit.getName());
+            change, user.getAccount(), newPatchSet, db, newCommit.getName());
+      }
+
+      if (!approvals.isEmpty()) {
+        hooks.doCommentAddedHook(change, user.getAccount(), newPatchSet,
+            null, approvals, db);
       }
 
       if (magicBranch != null && magicBranch.submit) {
@@ -2473,7 +2555,7 @@
       return;
     }
 
-    boolean defaultName = Strings.isNullOrEmpty(currentUser.getAccount().getFullName());
+    boolean defaultName = Strings.isNullOrEmpty(user.getAccount().getFullName());
     final RevWalk walk = rp.getRevWalk();
     walk.reset();
     walk.sort(RevSort.NONE);
@@ -2492,14 +2574,14 @@
           break;
         }
 
-        if (defaultName && currentUser.hasEmailAddress(
+        if (defaultName && user.hasEmailAddress(
               c.getCommitterIdent().getEmailAddress())) {
           try {
-            Account a = db.accounts().get(currentUser.getAccountId());
+            Account a = db.accounts().get(user.getAccountId());
             if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
               a.setFullName(c.getCommitterIdent().getName());
               db.accounts().update(Collections.singleton(a));
-              currentUser.getAccount().setFullName(a.getFullName());
+              user.getAccount().setFullName(a.getFullName());
               accountCache.evict(a.getId());
             }
           } catch (OrmException e) {
@@ -2523,7 +2605,7 @@
     }
 
     CommitReceivedEvent receiveEvent =
-        new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, currentUser);
+        new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, user);
     CommitValidators commitValidators =
         commitValidatorsFactory.create(ctl, sshInfo, repo);
 
@@ -2627,11 +2709,11 @@
     result.change = change;
     result.changeCtl = projectControl.controlFor(change);
     result.newPatchSet = ps;
-    result.info = patchSetInfoFactory.get(commit, psi);
+    result.info = patchSetInfoFactory.get(rp.getRevWalk(), commit, psi);
     result.mergedIntoRef = refName;
     markChangeMergedByPush(db, result, result.changeCtl);
     hooks.doChangeMergedHook(
-        change, currentUser.getAccount(), result.newPatchSet, db, commit.getName());
+        change, user.getAccount(), result.newPatchSet, db, commit.getName());
     sendMergedEmail(result);
     return change.getKey();
   }
@@ -2680,7 +2762,7 @@
       msgBuf.append(".");
       ChangeMessage msg = new ChangeMessage(
           new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)),
-          currentUser.getAccountId(), change.getLastUpdatedOn(),
+          user.getAccountId(), change.getLastUpdatedOn(),
           result.info.getKey());
       msg.setMessage(msgBuf.toString());
 
@@ -2703,7 +2785,7 @@
       public void run() {
         try {
           final MergedSender cm = mergedSenderFactory.create(id);
-          cm.setFrom(currentUser.getAccountId());
+          cm.setFrom(user.getAccountId());
           cm.setPatchSet(result.newPatchSet, result.info);
           cm.send();
         } catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index b2d3632..43a4d4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -28,11 +28,8 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
@@ -41,6 +38,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
@@ -89,15 +87,11 @@
         r.put(name, e.getValue());
       }
     }
-    rp.setAdvertisedRefs(r, advertiseHistory(r.values(), rp));
+    rp.setAdvertisedRefs(r, advertiseOpenChanges());
   }
 
-  private Set<ObjectId> advertiseHistory(
-      Iterable<Ref> sending,
-      BaseReceivePack rp) {
-    Set<ObjectId> toInclude = Sets.newHashSet();
-
-    // Advertise some recent open changes, in case a commit is based one.
+  private Set<ObjectId> advertiseOpenChanges() {
+    // Advertise some recent open changes, in case a commit is based on one.
     final int limit = 32;
     try {
       Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(limit);
@@ -110,68 +104,18 @@
           toGet.add(id);
         }
       }
+
+      Set<ObjectId> r = Sets.newHashSetWithExpectedSize(toGet.size());
       for (PatchSet ps : db.patchSets().get(toGet)) {
         if (ps.getRevision() != null && ps.getRevision().get() != null) {
-          toInclude.add(ObjectId.fromString(ps.getRevision().get()));
+          r.add(ObjectId.fromString(ps.getRevision().get()));
         }
       }
+      return r;
     } catch (OrmException err) {
       log.error("Cannot list open changes of " + projectName, err);
+      return Collections.emptySet();
     }
-
-    // Size of an additional ".have" line.
-    final int haveLineLen = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1;
-
-    // Maximum number of bytes to "waste" in the advertisement with
-    // a peek at this repository's current reachable history.
-    final int maxExtraSize = 8192;
-
-    // Number of recent commits to advertise immediately, hoping to
-    // show a client a nearby merge base.
-    final int base = 64;
-
-    // Number of commits to skip once base has already been shown.
-    final int step = 16;
-
-    // Total number of commits to extract from the history.
-    final int max = maxExtraSize / haveLineLen;
-
-    // Scan history until the advertisement is full.
-    Set<ObjectId> alreadySending = Sets.newHashSet();
-    RevWalk rw = rp.getRevWalk();
-    for (Ref ref : sending) {
-      try {
-        if (ref.getObjectId() != null) {
-          alreadySending.add(ref.getObjectId());
-          rw.markStart(rw.parseCommit(ref.getObjectId()));
-        }
-      } catch (IOException badCommit) {
-        continue;
-      }
-    }
-
-    int stepCnt = 0;
-    RevCommit c;
-    try {
-      while ((c = rw.next()) != null && toInclude.size() < max) {
-        if (alreadySending.contains(c)
-            || toInclude.contains(c)
-            || c.getParentCount() > 1) {
-          // Do nothing
-        } else if (toInclude.size() < base) {
-          toInclude.add(c);
-        } else {
-          stepCnt = ++stepCnt % step;
-          if (stepCnt == 0) {
-            toInclude.add(c);
-          }
-        }
-      }
-    } catch (IOException err) {
-      log.error("Error trying to advertise history on " + projectName, err);
-    }
-    rw.reset();
-    return toInclude;
   }
 
   private static boolean skip(String name) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
index 61cafc0..3c7666e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
@@ -332,7 +332,7 @@
 
   static boolean skip(Ref ref) {
     return ref.isSymbolic() || ref.getObjectId() == null
-        || PatchSet.isRef(ref.getName());
+        || PatchSet.isChangeRef(ref.getName());
   }
 
   private static boolean isTag(Ref ref) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/UpdateException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UpdateException.java
new file mode 100644
index 0000000..087af6c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UpdateException.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.server.git;
+
+/** Exception type thrown by {@link BatchUpdate} steps. */
+public class UpdateException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public UpdateException(String message) {
+    super(message);
+  }
+
+  public UpdateException(Throwable cause) {
+    super(cause);
+  }
+
+  public UpdateException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
similarity index 62%
copy from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
index cd01186..29b5373 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.server.git;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
+public class UserConfigSections {
+
+  /** The my menu user preferences. */
+  public static final String MY = "my";
+
+  /** The edit user preferences. */
+  public static final String EDIT = "edit";
+
+  private UserConfigSections() {
+  }
 }
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 dfde5d5..e7b98d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -50,8 +50,8 @@
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
-import java.util.Objects;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Support for metadata stored within a version controlled branch.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index a4684a1..46638f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -79,8 +79,8 @@
 
     Account.Id currAccountId;
     boolean canViewMetadata;
-    if (projectCtl.getCurrentUser().isIdentifiedUser()) {
-      IdentifiedUser user = ((IdentifiedUser) projectCtl.getCurrentUser());
+    if (projectCtl.getUser().isIdentifiedUser()) {
+      IdentifiedUser user = projectCtl.getUser().asIdentifiedUser();
       currAccountId = user.getAccountId();
       canViewMetadata = user.getCapabilities().canAccessDatabase();
     } else {
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 0e740f8..a2e9c03 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
@@ -16,14 +16,18 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.GroupCollector;
@@ -31,14 +35,15 @@
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeTip;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -50,16 +55,12 @@
 
 public class CherryPick extends SubmitStrategy {
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final GitReferenceUpdated gitRefUpdated;
   private final Map<Change.Id, CodeReviewCommit> newCommits;
 
   CherryPick(SubmitStrategy.Arguments args,
-      PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated) {
+      PatchSetInfoFactory patchSetInfoFactory) {
     super(args);
-
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.gitRefUpdated = gitRefUpdated;
     this.newCommits = new HashMap<>();
   }
 
@@ -68,148 +69,182 @@
       Collection<CodeReviewCommit> toMerge) throws MergeException {
     MergeTip mergeTip = new MergeTip(branchTip, toMerge);
     List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
-    while (!sorted.isEmpty()) {
-      CodeReviewCommit n = sorted.remove(0);
-      try {
-        if (mergeTip.getCurrentTip() == null) {
-          cherryPickUnbornRoot(n, mergeTip);
+    boolean first = true;
+    try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
+      while (!sorted.isEmpty()) {
+        CodeReviewCommit n = sorted.remove(0);
+        Change.Id cid = n.change().getId();
+        if (first && branchTip == null) {
+          u.addOp(cid, new CherryPickUnbornRootOp(mergeTip, n));
         } else if (n.getParentCount() == 0) {
-          cherryPickRootOntoBranch(n);
+          u.addOp(cid, new CherryPickRootOp(n));
         } else if (n.getParentCount() == 1) {
-          cherryPickOne(n, mergeTip);
+          u.addOp(cid, new CherryPickOneOp(mergeTip, n));
         } else {
-          cherryPickMultipleParents(n, mergeTip);
+          u.addOp(cid, new CherryPickMultipleParentsOp(mergeTip, n));
         }
-      } catch (NoSuchChangeException | IOException | OrmException e) {
-        throw new MergeException("Cannot merge " + n.name(), e);
+        first = false;
       }
+      u.execute();
+    } catch (UpdateException | RestApiException e) {
+      throw new MergeException("Cannot cherry-pick onto " + args.destBranch);
     }
+    // TODO(dborowitz): When BatchUpdate is hoisted out of CherryPick,
+    // SubmitStrategy should probably no longer return MergeTip, instead just
+    // mutating a single shared MergeTip passed in from the caller.
     return mergeTip;
   }
 
-  private void cherryPickUnbornRoot(CodeReviewCommit n, MergeTip mergeTip) {
-    // The branch is unborn. Take fast-forward resolution to create the branch.
-    mergeTip.moveTipTo(n, n);
-    n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
-  }
+  private static class CherryPickUnbornRootOp extends BatchUpdate.Op {
+    private final MergeTip mergeTip;
+    private final CodeReviewCommit toMerge;
 
-  private void cherryPickRootOntoBranch(CodeReviewCommit n) {
-    // Refuse to merge a root commit into an existing branch, we cannot obtain a
-    // delta for the cherry-pick to apply.
-    n.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
-  }
+    private CherryPickUnbornRootOp(MergeTip mergeTip,
+        CodeReviewCommit toMerge) {
+      this.mergeTip = mergeTip;
+      this.toMerge = toMerge;
+    }
 
-  private void cherryPickOne(CodeReviewCommit n, MergeTip mergeTip)
-      throws NoSuchChangeException, OrmException, IOException {
-    // If there is only one parent, a cherry-pick can be done by taking the
-    // delta relative to that one parent and redoing that on the current merge
-    // tip.
-    //
-    // Keep going in the case of a single merge failure; the goal is to
-    // cherry-pick as many commits as possible.
-    try {
-      CodeReviewCommit merge =
-          writeCherryPickCommit(mergeTip.getCurrentTip(), n);
-      mergeTip.moveTipTo(merge, merge);
-      newCommits.put(mergeTip.getCurrentTip().getPatchsetId()
-          .getParentKey(), mergeTip.getCurrentTip());
-    } catch (MergeConflictException mce) {
-      n.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
-    } catch (MergeIdenticalTreeException mie) {
-      n.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
+    @Override
+    public void updateRepo(RepoContext ctx) {
+      // The branch is unborn. Take fast-forward resolution to create the
+      // branch.
+      mergeTip.moveTipTo(toMerge, toMerge);
+      toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
     }
   }
 
-  private void cherryPickMultipleParents(CodeReviewCommit n, MergeTip mergeTip)
-      throws IOException, MergeException {
-    // There are multiple parents, so this is a merge commit. We don't want
-    // to cherry-pick the merge as clients can't easily rebase their history
-    // with that merge present and replaced by an equivalent merge with a
-    // different first parent. So instead behave as though MERGE_IF_NECESSARY
-    // was configured.
-    if (!args.mergeUtil.hasMissingDependencies(args.mergeSorter, n)) {
-      if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
-        mergeTip.moveTipTo(n, n);
-      } else {
-        PersonIdent myIdent = args.serverIdent.get();
-        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
-            myIdent, args.repo, args.rw, args.inserter,
-            args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(), n);
-        mergeTip.moveTipTo(result, n);
+  private static class CherryPickRootOp extends BatchUpdate.Op {
+    private final CodeReviewCommit toMerge;
+
+    private CherryPickRootOp(CodeReviewCommit toMerge) {
+      this.toMerge = toMerge;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx) {
+      // Refuse to merge a root commit into an existing branch, we cannot obtain
+      // a delta for the cherry-pick to apply.
+      toMerge.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
+    }
+  }
+
+  private class CherryPickOneOp extends BatchUpdate.Op {
+    private final MergeTip mergeTip;
+    private final CodeReviewCommit toMerge;
+
+    private PatchSet.Id psId;
+    private CodeReviewCommit newCommit;
+    private PatchSetInfo patchSetInfo;
+
+    private CherryPickOneOp(MergeTip mergeTip, CodeReviewCommit n) {
+      this.mergeTip = mergeTip;
+      this.toMerge = n;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx) throws IOException {
+      // If there is only one parent, a cherry-pick can be done by taking the
+      // delta relative to that one parent and redoing that on the current merge
+      // tip.
+      args.rw.parseBody(toMerge);
+      psId = ChangeUtil.nextPatchSetId(
+          args.repo, toMerge.change().currentPatchSetId());
+      String cherryPickCmtMsg =
+          args.mergeUtil.createCherryPickCommitMessage(toMerge);
+
+      PersonIdent committer = args.caller.newCommitterIdent(
+          ctx.getWhen(), args.serverIdent.get().getTimeZone());
+      try {
+        newCommit = args.mergeUtil.createCherryPickFromCommit(
+            args.repo, args.inserter, mergeTip.getCurrentTip(), toMerge,
+            committer, cherryPickCmtMsg, args.rw);
+        ctx.addRefUpdate(
+            new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName()));
+        patchSetInfo =
+            patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
+      } catch (MergeConflictException mce) {
+        // Keep going in the case of a single merge failure; the goal is to
+        // cherry-pick as many commits as possible.
+        toMerge.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
+      } catch (MergeIdenticalTreeException mie) {
+        toMerge.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
       }
-      args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
-          mergeTip.getCurrentTip(), args.alreadyAccepted);
-      setRefLogIdent();
-    } else {
-      // One or more dependencies were not met. The status was already marked on
-      // the commit so we have nothing further to perform at this time.
     }
-  }
 
-  private CodeReviewCommit writeCherryPickCommit(CodeReviewCommit mergeTip,
-      CodeReviewCommit n) throws IOException, OrmException,
-      NoSuchChangeException, MergeConflictException,
-      MergeIdenticalTreeException {
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException,
+         NoSuchChangeException {
+      if (newCommit == null) {
+        // Merge conflict; don't update change.
+        return;
+      }
+      PatchSet ps = new PatchSet(psId);
+      ps.setCreatedOn(ctx.getWhen());
+      ps.setUploader(args.caller.getAccountId());
+      ps.setRevision(new RevId(newCommit.getId().getName()));
 
-    args.rw.parseBody(n);
-
-    String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
-
-    PersonIdent committer = args.caller.newCommitterIdent(
-        TimeUtil.nowTs(), args.serverIdent.get().getTimeZone());
-    CodeReviewCommit newCommit =
-        (CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
-            args.inserter, mergeTip, n, committer, cherryPickCmtMsg, args.rw);
-
-    PatchSet.Id id =
-        ChangeUtil.nextPatchSetId(args.repo, n.change().currentPatchSetId());
-    PatchSet ps = new PatchSet(id);
-    ps.setCreatedOn(TimeUtil.nowTs());
-    ps.setUploader(args.caller.getAccountId());
-    ps.setRevision(new RevId(newCommit.getId().getName()));
-
-    RefUpdate ru;
-
-    args.db.changes().beginTransaction(n.change().getId());
-    try {
-      insertAncestors(args.db, ps.getId(), newCommit);
-      ps.setGroups(GroupCollector.getCurrentGroups(args.db, n.change()));
+      Change c = toMerge.change();
+      ps.setGroups(GroupCollector.getCurrentGroups(args.db, c));
       args.db.patchSets().insert(Collections.singleton(ps));
-      n.change()
-          .setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
-      args.db.changes().update(Collections.singletonList(n.change()));
+      insertAncestors(args.db, ps.getId(), newCommit);
+      c.setCurrentPatchSet(patchSetInfo);
+      args.db.changes().update(Collections.singletonList(c));
 
       List<PatchSetApproval> approvals = Lists.newArrayList();
       for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
-          args.db, n.getControl(), n.getPatchsetId())) {
+          args.db, toMerge.getControl(), toMerge.getPatchsetId())) {
         approvals.add(new PatchSetApproval(ps.getId(), a));
       }
       args.db.patchSetApprovals().insert(approvals);
 
-      ru = args.repo.updateRef(ps.getRefName());
-      ru.setExpectedOldObjectId(ObjectId.zeroId());
-      ru.setNewObjectId(newCommit);
-      ru.disableRefLog();
-      if (ru.update(args.rw) != RefUpdate.Result.NEW) {
-        throw new IOException(String.format(
-            "Failed to create ref %s in %s: %s", ps.getRefName(), n.change()
-                .getDest().getParentKey().get(), ru.getResult()));
-      }
+      newCommit.copyFrom(toMerge);
+      newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
+      newCommit.setControl(
+          args.changeControlFactory.controlFor(toMerge.change(), args.caller));
+      mergeTip.moveTipTo(newCommit, newCommit);
+      newCommits.put(c.getId(), newCommit);
+      setRefLogIdent();
+    }
+  }
 
-      args.db.commit();
-    } finally {
-      args.db.rollback();
+  private class CherryPickMultipleParentsOp extends BatchUpdate.Op {
+    private final MergeTip mergeTip;
+    private final CodeReviewCommit toMerge;
+
+    private CherryPickMultipleParentsOp(MergeTip mergeTip,
+        CodeReviewCommit toMerge) {
+      this.mergeTip = mergeTip;
+      this.toMerge = toMerge;
     }
 
-    gitRefUpdated.fire(n.change().getProject(), ru);
-
-    newCommit.copyFrom(n);
-    newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
-    newCommit.setControl(
-        args.changeControlFactory.controlFor(n.change(), args.caller));
-    newCommits.put(newCommit.getPatchsetId().getParentKey(), newCommit);
-    setRefLogIdent();
-    return newCommit;
+    @Override
+    public void updateRepo(RepoContext ctx) throws MergeException, IOException {
+      if (args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)) {
+        // One or more dependencies were not met. The status was already marked
+        // on the commit so we have nothing further to perform at this time.
+        return;
+      }
+      // There are multiple parents, so this is a merge commit. We don't want
+      // to cherry-pick the merge as clients can't easily rebase their history
+      // with that merge present and replaced by an equivalent merge with a
+      // different first parent. So instead behave as though MERGE_IF_NECESSARY
+      // was configured.
+      if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
+        mergeTip.moveTipTo(toMerge, toMerge);
+      } else {
+        PersonIdent myIdent =
+            new PersonIdent(args.serverIdent.get(), ctx.getWhen());
+        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
+            myIdent, args.repo, args.rw, args.inserter,
+            args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(),
+            toMerge);
+        mergeTip.moveTipTo(result, toMerge);
+      }
+      args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+          mergeTip.getCurrentTip(), args.alreadyAccepted);
+      setRefLogIdent();
+    }
   }
 
   private static void insertAncestors(ReviewDb db, PatchSet.Id id,
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 f9102ec..5ef33a6 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
@@ -15,19 +15,22 @@
 package com.google.gerrit.server.git.strategy;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
-import com.google.gerrit.server.change.RebaseChange;
+import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.RebaseSorter;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 
@@ -43,15 +46,15 @@
 
 public class RebaseIfNecessary extends SubmitStrategy {
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final RebaseChange rebaseChange;
+  private final RebaseChangeOp.Factory rebaseFactory;
   private final Map<Change.Id, CodeReviewCommit> newCommits;
 
   RebaseIfNecessary(SubmitStrategy.Arguments args,
       PatchSetInfoFactory patchSetInfoFactory,
-      RebaseChange rebaseChange) {
+      RebaseChangeOp.Factory rebaseFactory) {
     super(args);
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.rebaseChange = rebaseChange;
+    this.rebaseFactory = rebaseFactory;
     this.newCommits = new HashMap<>();
   }
 
@@ -84,11 +87,7 @@
 
         } else {
           try {
-            PatchSet newPatchSet =
-                rebaseChange.rebase(args.repo, args.rw, args.inserter,
-                    n.change(), n.getPatchsetId(), args.caller,
-                    mergeTip.getCurrentTip(), args.mergeUtil,
-                    args.serverIdent.get(), false, ValidatePolicy.NONE);
+            PatchSet newPatchSet = rebase(n, mergeTip);
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.approvalsUtil.byPatchSet(args.db,
                 n.getControl(), n.getPatchsetId())) {
@@ -97,11 +96,11 @@
             // rebaseChange.rebase() may already have copied some approvals,
             // use upsert, not insert, to avoid constraint violation on database
             args.db.patchSetApprovals().upsert(approvals);
-            CodeReviewCommit newTip = (CodeReviewCommit) args.rw.parseCommit(
+            CodeReviewCommit newTip = args.rw.parseCommit(
                 ObjectId.fromString(newPatchSet.getRevision().get()));
             mergeTip.moveTipTo(newTip, newTip);
             n.change().setCurrentPatchSet(
-                patchSetInfoFactory.get(mergeTip.getCurrentTip(),
+                patchSetInfoFactory.get(args.rw, mergeTip.getCurrentTip(),
                     newPatchSet.getId()));
             mergeTip.getCurrentTip().copyFrom(n);
             mergeTip.getCurrentTip().setControl(
@@ -112,10 +111,15 @@
             newCommits.put(newPatchSet.getId().getParentKey(),
                 mergeTip.getCurrentTip());
             setRefLogIdent();
-          } catch (MergeConflictException e) {
-            n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
+          } catch (UpdateException e) {
+            if (e.getCause() instanceof MergeConflictException) {
+              n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
+            }
+            throw new MergeException("Cannot rebase " + n.name(), e);
           } catch (NoSuchChangeException | OrmException | IOException
-              | InvalidChangeOperationException e) {
+              | RestApiException e) {
+            // TODO(dborowitz): Allow Submit to unwrap ResourceConflictException
+            // so it can turn into a 409.
             throw new MergeException("Cannot rebase " + n.name(), e);
           }
         }
@@ -163,6 +167,22 @@
     }
   }
 
+  private PatchSet rebase(CodeReviewCommit n, MergeTip mergeTip)
+      throws RestApiException, UpdateException, OrmException {
+    RebaseChangeOp op = rebaseFactory.create(
+          n.getControl(),
+          args.db.patchSets().get(n.getPatchsetId()),
+          mergeTip.getCurrentTip().name())
+        .setCommitterIdent(args.serverIdent.get())
+        .setRunHooks(false)
+        .setValidatePolicy(CommitValidators.Policy.NONE);
+    try (BatchUpdate bu = args.newBatchUpdate(TimeUtil.nowTs())) {
+      bu.addOp(n.change().getId(), op);
+      bu.execute();
+    }
+    return op.getPatchSet();
+  }
+
   @Override
   public Map<Change.Id, CodeReviewCommit> getNewCommits() {
     return newCommits;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index 4034abd..e3247c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -22,7 +22,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeSorter;
 import com.google.gerrit.server.git.MergeTip;
@@ -37,8 +39,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 
+import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
@@ -55,10 +57,11 @@
     protected final IdentifiedUser.GenericFactory identifiedUserFactory;
     protected final Provider<PersonIdent> serverIdent;
     protected final ReviewDb db;
+    protected final BatchUpdate.Factory batchUpdateFactory;
     protected final ChangeControl.GenericFactory changeControlFactory;
 
     protected final Repository repo;
-    protected final RevWalk rw;
+    protected final CodeReviewRevWalk rw;
     protected final ObjectInserter inserter;
     protected final RevFlag canMergeFlag;
     protected final Set<RevCommit> alreadyAccepted;
@@ -71,14 +74,16 @@
 
     Arguments(IdentifiedUser.GenericFactory identifiedUserFactory,
         Provider<PersonIdent> serverIdent, ReviewDb db,
+        BatchUpdate.Factory batchUpdateFactory,
         ChangeControl.GenericFactory changeControlFactory, Repository repo,
-        RevWalk rw, ObjectInserter inserter, RevFlag canMergeFlag,
+        CodeReviewRevWalk rw, ObjectInserter inserter, RevFlag canMergeFlag,
         Set<RevCommit> alreadyAccepted, Branch.NameKey destBranch,
         ApprovalsUtil approvalsUtil, MergeUtil mergeUtil,
         ChangeIndexer indexer, IdentifiedUser caller) {
       this.identifiedUserFactory = identifiedUserFactory;
       this.serverIdent = serverIdent;
       this.db = db;
+      this.batchUpdateFactory = batchUpdateFactory;
       this.changeControlFactory = changeControlFactory;
 
       this.repo = repo;
@@ -93,6 +98,12 @@
       this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
       this.caller = caller;
     }
+
+    BatchUpdate newBatchUpdate(Timestamp when) {
+      return batchUpdateFactory
+          .create(db, destBranch.getParentKey(), caller, when)
+          .setRepository(repo, rw, inserter);
+    }
   }
 
   protected final Arguments args;
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 b87499a..05aee25 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,8 +20,9 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.RebaseChange;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.ChangeIndexer;
@@ -39,7 +40,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -53,10 +53,10 @@
 
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Provider<PersonIdent> myIdent;
+  private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final RebaseChange rebaseChange;
+  private final RebaseChangeOp.Factory rebaseFactory;
   private final ProjectCache projectCache;
   private final ApprovalsUtil approvalsUtil;
   private final MergeUtil.Factory mergeUtilFactory;
@@ -66,39 +66,40 @@
   SubmitStrategyFactory(
       final IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritPersonIdent Provider<PersonIdent> myIdent,
+      final BatchUpdate.Factory batchUpdateFactory,
       final ChangeControl.GenericFactory changeControlFactory,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated gitRefUpdated, final RebaseChange rebaseChange,
+      final RebaseChangeOp.Factory rebaseFactory,
       final ProjectCache projectCache,
       final ApprovalsUtil approvalsUtil,
       final MergeUtil.Factory mergeUtilFactory,
       final ChangeIndexer indexer) {
     this.identifiedUserFactory = identifiedUserFactory;
     this.myIdent = myIdent;
+    this.batchUpdateFactory = batchUpdateFactory;
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.gitRefUpdated = gitRefUpdated;
-    this.rebaseChange = rebaseChange;
+    this.rebaseFactory = rebaseFactory;
     this.projectCache = projectCache;
     this.approvalsUtil = approvalsUtil;
     this.mergeUtilFactory = mergeUtilFactory;
     this.indexer = indexer;
   }
 
-  public SubmitStrategy create(final SubmitType submitType, final ReviewDb db,
-      final Repository repo, final RevWalk rw, final ObjectInserter inserter,
-      final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
-      final Branch.NameKey destBranch, final IdentifiedUser caller)
+  public SubmitStrategy create(SubmitType submitType, ReviewDb db,
+      Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
+      RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
+      Branch.NameKey destBranch, IdentifiedUser caller)
       throws MergeException, NoSuchProjectException {
     ProjectState project = getProject(destBranch);
-    final SubmitStrategy.Arguments args =
-        new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db,
-            changeControlFactory, repo, rw, inserter, canMergeFlag,
-            alreadyAccepted, destBranch,approvalsUtil,
-            mergeUtilFactory.create(project), indexer, caller);
+    SubmitStrategy.Arguments args = new SubmitStrategy.Arguments(
+        identifiedUserFactory, myIdent, db, batchUpdateFactory,
+        changeControlFactory, repo, rw, inserter, canMergeFlag, alreadyAccepted,
+        destBranch,approvalsUtil, mergeUtilFactory.create(project), indexer,
+        caller);
     switch (submitType) {
       case CHERRY_PICK:
-        return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
+        return new CherryPick(args, patchSetInfoFactory);
       case FAST_FORWARD_ONLY:
         return new FastForwardOnly(args);
       case MERGE_ALWAYS:
@@ -106,7 +107,7 @@
       case MERGE_IF_NECESSARY:
         return new MergeIfNecessary(args);
       case REBASE_IF_NECESSARY:
-        return new RebaseIfNecessary(args, patchSetInfoFactory, rebaseChange);
+        return new RebaseIfNecessary(args, patchSetInfoFactory, rebaseFactory);
       default:
         final String errorMsg = "No submit strategy for: " + submitType;
         log.error(errorMsg);
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 d8c3303..a48260e 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
@@ -14,12 +14,17 @@
 
 package com.google.gerrit.server.git.validators;
 
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
 import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.ChangeHookRunner.HookResult;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -39,6 +44,7 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 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.notes.NoteMap;
@@ -60,6 +66,17 @@
   private static final Logger log = LoggerFactory
       .getLogger(CommitValidators.class);
 
+  public static enum Policy {
+    /** Use {@link #validateForGerritCommits}. */
+    GERRIT,
+
+    /** Use {@link #validateForReceiveCommits}. */
+    RECEIVE_COMMITS,
+
+    /** Do not validate commits. */
+    NONE
+  }
+
   public interface Factory {
     CommitValidators create(RefControl refControl, SshInfo sshInfo,
         Repository repo);
@@ -71,6 +88,7 @@
   private final String installCommitMsgHookCommand;
   private final SshInfo sshInfo;
   private final Repository repo;
+  private final ChangeHooks hooks;
   private final DynamicSet<CommitValidationListener> commitValidationListeners;
 
   @Inject
@@ -78,6 +96,7 @@
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @GerritServerConfig final Config config,
       final DynamicSet<CommitValidationListener> commitValidationListeners,
+      final ChangeHooks hooks,
       @Assisted final SshInfo sshInfo,
       @Assisted final Repository repo, @Assisted final RefControl refControl) {
     this.gerritIdent = gerritIdent;
@@ -87,6 +106,7 @@
         config.getString("gerrit", null, "installCommitMsgHookCommand");
     this.sshInfo = sshInfo;
     this.repo = repo;
+    this.hooks = hooks;
     this.commitValidationListeners = commitValidationListeners;
   }
 
@@ -144,6 +164,7 @@
     }
     validators.add(new ConfigValidator(refControl, repo));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
+    validators.add(new ChangeHookValidator(refControl, hooks));
 
     List<CommitValidationMessage> messages = new LinkedList<>();
 
@@ -172,7 +193,7 @@
       this.canonicalWebUrl = canonicalWebUrl;
       this.installCommitMsgHookCommand = installCommitMsgHookCommand;
       this.sshInfo = sshInfo;
-      this.user = (IdentifiedUser) projectControl.getCurrentUser();
+      this.user = projectControl.getUser().asIdentifiedUser();
     }
 
     @Override
@@ -295,9 +316,9 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
 
-      if (RefNames.REFS_CONFIG.equals(refControl.getRefName())) {
+      if (REFS_CONFIG.equals(refControl.getRefName())) {
         List<CommitValidationMessage> messages = new LinkedList<>();
 
         try {
@@ -381,7 +402,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
       final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
       final PersonIdent author = receiveEvent.commit.getAuthorIdent();
       final ProjectControl projectControl = refControl.getProjectControl();
@@ -424,7 +445,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
       final PersonIdent author = receiveEvent.commit.getAuthorIdent();
 
       if (!currentUser.hasEmailAddress(author.getEmailAddress())
@@ -454,7 +475,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
       final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
       if (!currentUser.hasEmailAddress(committer.getEmailAddress())
           && !refControl.canForgeCommitter()) {
@@ -524,6 +545,48 @@
     }
   }
 
+  /** Reject commits that don't pass user-supplied ref-update hook. */
+  public static class ChangeHookValidator implements
+      CommitValidationListener {
+    private final RefControl refControl;
+    private final ChangeHooks hooks;
+
+    public ChangeHookValidator(RefControl refControl, ChangeHooks hooks) {
+      this.refControl = refControl;
+      this.hooks = hooks;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+
+      if (refControl.getUser().isIdentifiedUser()) {
+        IdentifiedUser user = refControl.getUser().asIdentifiedUser();
+
+        String refname = receiveEvent.refName;
+        ObjectId old = receiveEvent.commit.getParent(0);
+
+        if (receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
+          /*
+           * If the ref-update hook tries to distinguish behavior between pushes to
+           * refs/heads/... and refs/for/..., make sure we send it the correct refname.
+           * Also, if this is targetting refs/for/, make sure we behave the same as
+           * what a push to refs/for/ would behave; in particular, setting oldrev to
+           * 0000000000000000000000000000000000000000.
+           */
+          refname = refname.replace(R_HEADS, "refs/for/refs/heads/");
+          old = ObjectId.zeroId();
+        }
+        HookResult result = hooks.doRefUpdateHook(receiveEvent.project, refname,
+            user.getAccount(), old, receiveEvent.commit);
+        if (result != null && result.getExitValue() != 0) {
+            throw new CommitValidationException(result.toString().trim());
+        }
+      }
+      return Collections.emptyList();
+    }
+  }
+
   private static CommitValidationMessage getInvalidEmailError(RevCommit c, String type,
       PersonIdent who, IdentifiedUser currentUser, String canonicalWebUrl) {
     StringBuilder sb = new StringBuilder();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
index fefe02a..2c032c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
@@ -49,7 +49,7 @@
    *        These may be RevObject or RevCommit if the processor parsed them.
    *        Implementors should not rely on the values being parsed.
    * @throws ValidationException to block the upload and send a message
-   *         back to the end-used over the client's protocol connection.
+   *         back to the end-user over the client's protocol connection.
    */
   public void onPreUpload(Repository repository, Project project,
       String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
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 5155e25..c274a37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
@@ -101,7 +100,7 @@
     GroupControl control = resource.getControl();
     Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = Maps.newHashMap();
     List<GroupInfo> result = Lists.newLinkedList();
-    Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
+    Account.Id me = control.getUser().getAccountId();
 
     for (String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
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 0986584..4e67c02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -96,7 +95,7 @@
 
   @Override
   public GroupInfo apply(TopLevelResource resource, GroupInput input)
-      throws AuthException, BadRequestException, UnprocessableEntityException,
+      throws BadRequestException, UnprocessableEntityException,
       ResourceConflictException, OrmException {
     if (input == null) {
       input = new GroupInput();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index 3d99565..bde8fb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
@@ -110,7 +109,7 @@
   }
 
   private void writeAudits(final List<AccountGroupById> toRemoved) {
-    final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
+    final Account.Id me = self.get().getAccountId();
     auditService.dispatchDeleteGroupsFromGroup(me, toRemoved);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index 3047994..b14974b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupControl;
@@ -97,7 +96,7 @@
   }
 
   private void writeAudits(final List<AccountGroupMember> toRemove) {
-    final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
+    final Account.Id me = self.get().getAccountId();
     auditService.dispatchDeleteAccountsFromGroup(me, toRemove);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index bf5193f..f3a2ea2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -16,14 +16,17 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
@@ -32,6 +35,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GetGroups;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupComparator;
 import com.google.gerrit.server.account.GroupControl;
@@ -64,6 +68,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<GetGroups> accountGetGroups;
   private final GroupJson json;
+  private final GroupBackend groupBackend;
 
   private EnumSet<ListGroupsOption> options =
       EnumSet.noneOf(ListGroupsOption.class);
@@ -73,6 +78,7 @@
   private int limit;
   private int start;
   private String matchSubstring;
+  private String suggest;
 
   @Option(name = "--project", aliases = {"-p"},
       usage = "projects for which the groups should be listed")
@@ -121,6 +127,11 @@
     this.matchSubstring = matchSubstring;
   }
 
+  @Option(name = "--suggest", usage = "to get a suggestion of groups")
+  public void setSuggest(String suggest) {
+    this.suggest = suggest;
+  }
+
   @Option(name = "-o", usage = "Output options per group")
   void addOption(ListGroupsOption o) {
     options.add(o);
@@ -137,7 +148,8 @@
       final GroupControl.GenericFactory genericGroupControlFactory,
       final Provider<IdentifiedUser> identifiedUser,
       final IdentifiedUser.GenericFactory userFactory,
-      final Provider<GetGroups> accountGetGroups, GroupJson json) {
+      final Provider<GetGroups> accountGetGroups, GroupJson json,
+      GroupBackend groupBackend) {
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
     this.genericGroupControlFactory = genericGroupControlFactory;
@@ -145,6 +157,7 @@
     this.userFactory = userFactory;
     this.accountGetGroups = accountGetGroups;
     this.json = json;
+    this.groupBackend = groupBackend;
   }
 
   public void setOptions(EnumSet<ListGroupsOption> options) {
@@ -161,7 +174,7 @@
 
   @Override
   public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
-      throws OrmException {
+      throws OrmException, BadRequestException {
     SortedMap<String, GroupInfo> output = Maps.newTreeMap();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(
@@ -172,53 +185,107 @@
     return output;
   }
 
-  public List<GroupInfo> get() throws OrmException {
-    List<GroupInfo> groupInfos;
+  public List<GroupInfo> get() throws OrmException, BadRequestException {
+    if (!Strings.isNullOrEmpty(suggest)) {
+      return suggestGroups();
+    }
+
+    if (owned) {
+      return getGroupsOwnedBy(
+          user != null ? userFactory.create(user) : identifiedUser.get());
+    }
+
     if (user != null) {
-      if (owned) {
-        groupInfos = getGroupsOwnedBy(userFactory.create(user));
-      } else {
-        groupInfos = accountGetGroups.get().apply(
-            new AccountResource(userFactory.create(user)));
+      return accountGetGroups.get().apply(
+          new AccountResource(userFactory.create(user)));
+    }
+
+    return getAllGroups();
+  }
+
+  private List<GroupInfo> getAllGroups() throws OrmException {
+    List<GroupInfo> groupInfos;
+    List<AccountGroup> groupList;
+    if (!projects.isEmpty()) {
+      Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
+      for (final ProjectControl projectControl : projects) {
+        final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
+        for (final GroupReference groupRef : groupsRefs) {
+          final AccountGroup group = groupCache.get(groupRef.getUUID());
+          if (group != null) {
+            groups.put(group.getGroupUUID(), group);
+          }
+        }
       }
+      groupList = filterGroups(groups.values());
     } else {
-      if (owned) {
-        groupInfos = getGroupsOwnedBy(identifiedUser.get());
-      } else {
-        List<AccountGroup> groupList;
-        if (!projects.isEmpty()) {
-          Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
-          for (final ProjectControl projectControl : projects) {
-            final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
-            for (final GroupReference groupRef : groupsRefs) {
-              final AccountGroup group = groupCache.get(groupRef.getUUID());
-              if (group != null) {
-                groups.put(group.getGroupUUID(), group);
-              }
-            }
-          }
-          groupList = filterGroups(groups.values());
-        } else {
-          groupList = filterGroups(groupCache.all());
-        }
-        groupInfos = Lists.newArrayListWithCapacity(groupList.size());
-        int found = 0;
-        int foundIndex = 0;
-        for (AccountGroup group : groupList) {
-          if (foundIndex++ < start) {
-            continue;
-          }
-          if (limit > 0 && ++found > limit) {
-            break;
-          }
-          groupInfos.add(json.addOptions(options).format(
-              GroupDescriptions.forAccountGroup(group)));
-        }
+      groupList = filterGroups(groupCache.all());
+    }
+    groupInfos = Lists.newArrayListWithCapacity(groupList.size());
+    int found = 0;
+    int foundIndex = 0;
+    for (AccountGroup group : groupList) {
+      if (foundIndex++ < start) {
+        continue;
+      }
+      if (limit > 0 && ++found > limit) {
+        break;
+      }
+      groupInfos.add(json.addOptions(options).format(
+          GroupDescriptions.forAccountGroup(group)));
+    }
+    return groupInfos;
+  }
+
+  private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException {
+    if (conflictingSuggestParameters()) {
+      throw new BadRequestException(
+          "You should only have no more than one --project and -n with --suggest");
+    }
+
+    List<GroupReference> groupRefs = Lists.newArrayList(Iterables.limit(
+        groupBackend.suggest(
+            suggest, Iterables.getFirst(projects, null)),
+        limit <= 0 ? 10 : Math.min(limit, 10)));
+
+    List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
+    for (final GroupReference ref : groupRefs) {
+      GroupDescription.Basic desc = groupBackend.get(ref.getUUID());
+      if (desc != null) {
+        groupInfos.add(json.addOptions(options).format(desc));
       }
     }
     return groupInfos;
   }
 
+  private boolean conflictingSuggestParameters() {
+    if (Strings.isNullOrEmpty(suggest)) {
+      return false;
+    }
+    if (projects.size() > 1) {
+      return true;
+    }
+    if (visibleToAll) {
+      return true;
+    }
+    if (user != null) {
+      return true;
+    }
+    if (owned) {
+      return true;
+    }
+    if (start != 0) {
+      return true;
+    }
+    if (!groupsToInspect.isEmpty()) {
+      return true;
+    }
+    if (!Strings.isNullOrEmpty(matchSubstring)) {
+      return true;
+    }
+    return false;
+  }
+
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
       throws OrmException {
     List<GroupInfo> groups = Lists.newArrayList();
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 ba28cd1..8266162 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
@@ -71,8 +70,7 @@
   @Override
   public String apply(GroupResource rsrc, Input input)
       throws MethodNotAllowedException, AuthException, BadRequestException,
-      ResourceNotFoundException, ResourceConflictException, OrmException,
-      NoSuchGroupException {
+      ResourceConflictException, OrmException, NoSuchGroupException {
     if (rsrc.toAccountGroup() == null) {
       throw new MethodNotAllowedException();
     } else if (!rsrc.getControl().isOwner()) {
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 e711306..4fa5cd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -207,6 +207,21 @@
         }
       };
 
+  /** Submission id assigned by MergeOp. */
+  public static final FieldDef<ChangeData, String> SUBMISSIONID =
+      new FieldDef.Single<ChangeData, String>(
+          "submissionid", FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getSubmissionId();
+        }
+      };
+
   /** Last update time since January 1, 1970. */
   public static final FieldDef<ChangeData, Timestamp> UPDATED =
       new FieldDef.Single<ChangeData, Timestamp>(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index 5cb5c65..6c484d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -63,6 +63,15 @@
         IndexCollection indexes);
   }
 
+  public static CheckedFuture<?, IOException> allAsList(
+      List<? extends ListenableFuture<?>> futures) {
+    // allAsList propagates the first seen exception, wrapped in
+    // ExecutionException, so we can reuse the same mapper as for a single
+    // future. Assume the actual contents of the exception are not useful to
+    // callers. All exceptions are already logged by IndexTask.
+    return Futures.makeChecked(Futures.allAsList(futures), MAPPER);
+  }
+
   private static final Function<Exception, IOException> MAPPER =
       new Function<Exception, IOException>() {
     @Override
@@ -136,11 +145,7 @@
     for (Change.Id id : ids) {
       futures.add(indexAsync(id));
     }
-    // allAsList propagates the first seen exception, wrapped in
-    // ExecutionException, so we can reuse the same mapper as for a single
-    // future. Assume the actual contents of the exception are not useful to
-    // callers. All exceptions are already logged by IndexTask.
-    return Futures.makeChecked(Futures.allAsList(futures), MAPPER);
+    return allAsList(futures);
   }
 
   /**
@@ -226,7 +231,7 @@
           }
 
           @Override
-          public CurrentUser getCurrentUser() {
+          public CurrentUser getUser() {
             throw new OutOfScopeException("No user during ChangeIndexer");
           }
         };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index a8a97a8..4789a14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -377,6 +377,42 @@
       ChangeField.AUTHOR,
       ChangeField.COMMITTER);
 
+  static final Schema<ChangeData> V25 = schema(
+      ChangeField.LEGACY_ID2,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.SUBMISSIONID,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT,
+      ChangeField.AUTHOR,
+      ChangeField.COMMITTER);
+
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
   }
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 e79dc63..00aa081 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
@@ -29,24 +29,27 @@
  */
 @AutoValue
 public abstract class IndexConfig {
+  private static final int DEFAULT_MAX_TERMS = 500;
   private static final int DEFAULT_MAX_PREFIX_TERMS = 100;
 
   public static IndexConfig createDefault() {
-    return create(0, 0, DEFAULT_MAX_PREFIX_TERMS);
+    return create(0, 0, DEFAULT_MAX_TERMS, DEFAULT_MAX_PREFIX_TERMS);
   }
 
   public static IndexConfig fromConfig(Config cfg) {
     return create(
         cfg.getInt("index", null, "maxLimit", 0),
         cfg.getInt("index", null, "maxPages", 0),
+        cfg.getInt("index", null, "maxTerms", 0),
         cfg.getInt("index", null, "maxPrefixTerms", DEFAULT_MAX_PREFIX_TERMS));
   }
 
   public static IndexConfig create(int maxLimit, int maxPages,
-      int maxPrefixTerms) {
+      int maxTerms, int maxPrefixTerms) {
     return new AutoValue_IndexConfig(
         checkLimit(maxLimit, "maxLimit", Integer.MAX_VALUE),
         checkLimit(maxPages, "maxPages", Integer.MAX_VALUE),
+        checkLimit(maxTerms, "maxTerms", Integer.MAX_VALUE),
         checkLimit(maxPrefixTerms, "maxPrefixTerms", DEFAULT_MAX_PREFIX_TERMS));
   }
 
@@ -71,6 +74,12 @@
   public abstract int maxPages();
 
   /**
+   * @return maximum number of total index query terms supported by the
+   *     underlying index, or limited for performance reasons.
+   */
+  public abstract int maxTerms();
+
+  /**
    * @return maximum number of prefix terms per query supported by the
    *     underlying index, or limited for performance reasons. Not enforced for
    *     general queries; only for specific cases where the query system can
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
index 27df8c2..8dd7aa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
@@ -33,6 +33,8 @@
 import com.google.gerrit.server.query.change.OrSource;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.util.MutableInteger;
+
 import java.util.BitSet;
 import java.util.EnumSet;
 import java.util.List;
@@ -117,10 +119,13 @@
   }
 
   private final IndexCollection indexes;
+  private final IndexConfig config;
 
   @Inject
-  IndexRewriteImpl(IndexCollection indexes) {
+  IndexRewriteImpl(IndexCollection indexes,
+      IndexConfig config) {
     this.indexes = indexes;
+    this.config = config;
   }
 
   @Override
@@ -132,7 +137,8 @@
     // skipped results would have been filtered out by the enclosing AndSource.
     limit += start;
 
-    Predicate<ChangeData> out = rewriteImpl(in, index, limit);
+    MutableInteger leafTerms = new MutableInteger();
+    Predicate<ChangeData> out = rewriteImpl(in, index, limit, leafTerms);
     if (in == out || out instanceof IndexPredicate) {
       return new IndexedChangeQuery(index, out, limit);
     } else if (out == null /* cannot rewrite */) {
@@ -148,6 +154,7 @@
    * @param in predicate to rewrite.
    * @param index index whose schema determines which fields are indexed.
    * @param limit maximum number of results to return.
+   * @param leafTerms number of leaf index query terms encountered so far.
    * @return {@code null} if no part of this subtree can be queried in the
    *     index directly. {@code in} if this subtree and all its children can be
    *     queried directly in the index. Otherwise, a predicate that is
@@ -157,8 +164,12 @@
    *     support this predicate.
    */
   private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in,
-      ChangeIndex index, int limit) throws QueryParseException {
+      ChangeIndex index, int limit, MutableInteger leafTerms)
+      throws QueryParseException {
     if (isIndexPredicate(in, index)) {
+      if (++leafTerms.value > config.maxTerms()) {
+        throw new QueryParseException("too many terms in query");
+      }
       return in;
     } else if (in instanceof LimitPredicate) {
       // Replace any limits with the limit provided by the caller.
@@ -174,7 +185,7 @@
     List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
     for (int i = 0; i < n; i++) {
       Predicate<ChangeData> c = in.getChild(i);
-      Predicate<ChangeData> nc = rewriteImpl(c, index, limit);
+      Predicate<ChangeData> nc = rewriteImpl(c, index, limit, leafTerms);
       if (nc == c) {
         isIndexed.set(i);
         newChildren.add(c);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
index 4210363..8ea6653 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
@@ -29,6 +29,8 @@
 
 package com.google.gerrit.server.ioutil;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.reviewdb.client.CodedEnum;
 
 import org.eclipse.jgit.util.IO;
@@ -139,7 +141,7 @@
     if (bin.length == 0) {
       return null;
     }
-    return new String(bin, 0, bin.length, "UTF-8");
+    return new String(bin, 0, bin.length, UTF_8);
   }
 
   /** Write a UTF-8 string, prefixed by its byte length in a varint. */
@@ -148,7 +150,7 @@
     if (s == null) {
       writeVarInt32(output, 0);
     } else {
-      writeBytes(output, s.getBytes("UTF-8"));
+      writeBytes(output, s.getBytes(UTF_8));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 4e9ed2b..863cb82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.mail;
 
-import java.io.UnsupportedEncodingException;
-
 public class Address {
   public static Address parse(final String in) {
     final int lt = in.indexOf('<');
@@ -69,14 +67,10 @@
 
   @Override
   public String toString() {
-    try {
-      return toHeaderString();
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("Cannot encode address", e);
-    }
+    return toHeaderString();
   }
 
-  public String toHeaderString() throws UnsupportedEncodingException {
+  public String toHeaderString() {
     if (name != null) {
       return quotedPhrase(name) + " <" + email + ">";
     } else if (isSimple()) {
@@ -98,8 +92,7 @@
     return true;
   }
 
-  private static String quotedPhrase(final String name)
-      throws UnsupportedEncodingException {
+  private static String quotedPhrase(final String name) {
     if (EmailHeader.needsQuotedPrintable(name)) {
       return EmailHeader.quotedPrintable(name);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
index 592fbb0..30026bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -104,8 +105,7 @@
     }
   }
 
-  static java.lang.String quotedPrintable(java.lang.String value)
-      throws UnsupportedEncodingException {
+  static java.lang.String quotedPrintable(java.lang.String value) {
     final StringBuilder r = new StringBuilder();
 
     r.append("=?UTF-8?Q?");
@@ -115,7 +115,7 @@
         r.append('_');
 
       } else if (needsQuotedPrintableWithinPhrase(cp)) {
-        byte[] buf = new java.lang.String(Character.toChars(cp)).getBytes("UTF-8");
+        byte[] buf = new java.lang.String(Character.toChars(cp)).getBytes(UTF_8);
         for (byte b: buf) {
           r.append('=');
           r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase());
@@ -151,7 +151,7 @@
     public void write(Writer w) throws IOException {
       final SimpleDateFormat fmt;
       // Mon, 1 Jun 2009 10:49:44 -0700
-      fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
+      fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US);
       w.write(fmt.format(value));
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index 8501426..58bdac1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -26,13 +26,13 @@
    * @param emailAddress the address to add.
    * @return an unforgeable string to email to {@code emailAddress}. Presenting
    *         the string provides proof the user has the ability to read messages
-   *         sent to that address.
+   *         sent to that address. Must not be null.
    */
   public String encode(Account.Id accountId, String emailAddress);
 
   /**
    * Decode a token previously created.
-   * @param tokenString the string created by encode.
+   * @param tokenString the string created by encode. Never null.
    * @return a pair of account id and email address.
    * @throws InvalidTokenException the token is invalid, expired, malformed, etc.
    */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index a2f369b..7dd51fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -443,7 +445,7 @@
       if (runtime.getLoaderNameForResource(name) == null) {
         name = "com/google/gerrit/server/mail/" + name;
       }
-      Template template = runtime.getTemplate(name, "UTF-8");
+      Template template = runtime.getTemplate(name, UTF_8.name());
       StringWriter w = new StringWriter();
       template.merge(velocityContext, w);
       return w.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
deleted file mode 100644
index 80a5d24..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.mail;
-
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.MailUtil.MailRecipients;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.FooterLine;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-
-public class PatchSetNotificationSender {
-  private static final Logger log =
-      LoggerFactory.getLogger(PatchSetNotificationSender.class);
-
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager repoManager;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ApprovalsUtil approvalsUtil;
-  private final AccountResolver accountResolver;
-  private final CreateChangeSender.Factory createChangeSenderFactory;
-  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
-
-  @Inject
-  public PatchSetNotificationSender(Provider<ReviewDb> db,
-      GitRepositoryManager repoManager,
-      PatchSetInfoFactory patchSetInfoFactory,
-      ApprovalsUtil approvalsUtil,
-      AccountResolver accountResolver,
-      CreateChangeSender.Factory createChangeSenderFactory,
-      ReplacePatchSetSender.Factory replacePatchSetFactory) {
-    this.db = db;
-    this.repoManager = repoManager;
-    this.patchSetInfoFactory = patchSetInfoFactory;
-    this.approvalsUtil = approvalsUtil;
-    this.accountResolver = accountResolver;
-    this.createChangeSenderFactory = createChangeSenderFactory;
-    this.replacePatchSetFactory = replacePatchSetFactory;
-  }
-
-  public void send(final ChangeNotes notes, final ChangeUpdate update,
-      final boolean newChange, final IdentifiedUser currentUser,
-      final Change updatedChange, final PatchSet updatedPatchSet,
-      final LabelTypes labelTypes)
-      throws OrmException, IOException {
-    try (Repository git = repoManager.openRepository(updatedChange.getProject())) {
-      final RevCommit commit;
-      try (RevWalk revWalk = new RevWalk(git)) {
-        commit = revWalk.parseCommit(ObjectId.fromString(
-            updatedPatchSet.getRevision().get()));
-      }
-      final PatchSetInfo info = patchSetInfoFactory.get(commit, updatedPatchSet.getId());
-      final List<FooterLine> footerLines = commit.getFooterLines();
-      final Account.Id me = currentUser.getAccountId();
-      final MailRecipients recipients =
-          getRecipientsFromFooters(accountResolver, updatedPatchSet, footerLines);
-      recipients.remove(me);
-
-      if (newChange) {
-        approvalsUtil.addReviewers(db.get(), update, labelTypes, updatedChange,
-            updatedPatchSet, info, recipients.getReviewers(),
-            Collections.<Account.Id> emptySet());
-        try {
-          CreateChangeSender cm =
-              createChangeSenderFactory.create(updatedChange.getId());
-          cm.setFrom(me);
-          cm.setPatchSet(updatedPatchSet, info);
-          cm.addReviewers(recipients.getReviewers());
-          cm.addExtraCC(recipients.getCcOnly());
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new change " + updatedChange.getId(), e);
-        }
-      } else {
-        approvalsUtil.addReviewers(db.get(), update, labelTypes, updatedChange,
-            updatedPatchSet, info, recipients.getReviewers(),
-            approvalsUtil.getReviewers(db.get(), notes).values());
-        final ChangeMessage msg =
-            new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
-                ChangeUtil.messageUUID(db.get())), me,
-                updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
-        msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
-        try {
-          ReplacePatchSetSender cm =
-              replacePatchSetFactory.create(updatedChange.getId());
-          cm.setFrom(me);
-          cm.setPatchSet(updatedPatchSet, info);
-          cm.setChangeMessage(msg);
-          cm.addReviewers(recipients.getReviewers());
-          cm.addExtraCC(recipients.getCcOnly());
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index beada69..c4d374f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
@@ -63,7 +65,8 @@
 
   public String getEmailRegistrationToken() {
     if (emailToken == null) {
-      emailToken = tokenVerifier.encode(user.getAccountId(), addr);
+      emailToken = checkNotNull(
+          tokenVerifier.encode(user.getAccountId(), addr), "token");
     }
     return emailToken;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index 29bce1c..f12859f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtjsonrpc.server.SignedToken;
@@ -25,7 +27,6 @@
 
 import org.eclipse.jgit.util.Base64;
 
-import java.io.UnsupportedEncodingException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -50,10 +51,10 @@
   public String encode(Account.Id accountId, String emailAddress) {
     try {
       String payload = String.format("%s:%s", accountId, emailAddress);
-      byte[] utf8 = payload.getBytes("UTF-8");
+      byte[] utf8 = payload.getBytes(UTF_8);
       String base64 = Base64.encodeBytes(utf8);
       return emailRegistrationToken.newToken(base64);
-    } catch (XsrfException | UnsupportedEncodingException e) {
+    } catch (XsrfException e) {
       throw new IllegalArgumentException(e);
     }
   }
@@ -70,13 +71,7 @@
       throw new InvalidTokenException();
     }
 
-    String payload;
-    try {
-      payload = new String(Base64.decode(token.getData()), "UTF-8");
-    } catch (UnsupportedEncodingException err) {
-      throw new InvalidTokenException(err);
-    }
-
+    String payload = new String(Base64.decode(token.getData()), UTF_8);
     Matcher matcher = Pattern.compile("^([0-9]+):(.+@.+)$").matcher(payload);
     if (!matcher.matches()) {
       throw new InvalidTokenException();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index 785093a..8bd6633 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
@@ -241,7 +243,7 @@
   }
 
   private SMTPClient open() throws EmailException {
-    final AuthSMTPClient client = new AuthSMTPClient("UTF-8");
+    final AuthSMTPClient client = new AuthSMTPClient(UTF_8.name());
 
     if (smtpEncryption == Encryption.SSL) {
       client.enableSSL(sslVerify);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 96322d2..fb41027 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -78,7 +78,7 @@
   }
 
   public IdentifiedUser getUser() {
-    return (IdentifiedUser) ctl.getCurrentUser();
+    return ctl.getUser().asIdentifiedUser();
   }
 
   public PatchSet.Id getPatchSetId() {
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 47c1731..bd8f797 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -93,9 +93,9 @@
         anonymousCowardName, when);
     this.draftsProject = allUsers;
     this.commentsUtil = commentsUtil;
-    checkState(ctl.getCurrentUser().isIdentifiedUser(),
+    checkState(ctl.getUser().isIdentifiedUser(),
         "Current user must be identified");
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    IdentifiedUser user = ctl.getUser().asIdentifiedUser();
     this.accountId = user.getAccountId();
     this.changeNotes = getChangeNotes().load();
     this.draftNotes = draftNotesFactory.create(ctl.getChange().getId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index e6d9ff8..d63f972 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -84,11 +84,15 @@
 
   public static Config allEnabledConfig() {
     Config cfg = new Config();
+    setAllEnabledConfig(cfg);
+    return cfg;
+  }
+
+  public static void setAllEnabledConfig(Config cfg) {
     for (Table t : Table.values()) {
       cfg.setBoolean(NOTEDB, t.key(), WRITE, true);
       cfg.setBoolean(NOTEDB, t.key(), READ, true);
     }
-    return cfg;
   }
 
   private final boolean writeChanges;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index e7c56be..5de28f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -16,10 +16,12 @@
 
 import static com.google.gerrit.server.ioutil.BasicSerialization.readBytes;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readEnum;
+import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 
@@ -49,7 +51,7 @@
 
   static PatchListEntry empty(final String fileName) {
     return new PatchListEntry(ChangeType.MODIFIED, PatchType.UNIFIED, null,
-        fileName, EMPTY_HEADER, Collections.<Edit> emptyList(), 0, 0);
+        fileName, EMPTY_HEADER, Collections.<Edit> emptyList(), 0, 0, 0);
   }
 
   private final ChangeType changeType;
@@ -60,8 +62,11 @@
   private final List<Edit> edits;
   private final int insertions;
   private final int deletions;
+  private final long sizeDelta;
+  // Note: When adding new fields, the serialVersionUID in PatchListKey must be
+  // incremented so that entries from the cache are automatically invalidated.
 
-  PatchListEntry(final FileHeader hdr, List<Edit> editList) {
+  PatchListEntry(FileHeader hdr, List<Edit> editList, long sizeDelta) {
     changeType = toChangeType(hdr);
     patchType = toPatchType(hdr);
 
@@ -106,12 +111,12 @@
     }
     insertions = ins;
     deletions = del;
+    this.sizeDelta = sizeDelta;
   }
 
-  private PatchListEntry(final ChangeType changeType,
-      final PatchType patchType, final String oldName, final String newName,
-      final byte[] header, final List<Edit> edits, final int insertions,
-      final int deletions) {
+  private PatchListEntry(ChangeType changeType, PatchType patchType,
+      String oldName, String newName, byte[] header, List<Edit> edits,
+      int insertions, int deletions, long sizeDelta) {
     this.changeType = changeType;
     this.patchType = patchType;
     this.oldName = oldName;
@@ -120,6 +125,7 @@
     this.edits = edits;
     this.insertions = insertions;
     this.deletions = deletions;
+    this.sizeDelta = sizeDelta;
   }
 
   int weigh() {
@@ -166,6 +172,10 @@
     return deletions;
   }
 
+  public long getSizeDelta() {
+    return sizeDelta;
+  }
+
   public List<String> getHeaderLines() {
     final IntList m = RawParseUtils.lineMap(header, 0, header.length);
     final List<String> headerLines = new ArrayList<>(m.size() - 1);
@@ -190,7 +200,7 @@
     return p;
   }
 
-  void writeTo(final OutputStream out) throws IOException {
+  void writeTo(OutputStream out) throws IOException {
     writeEnum(out, changeType);
     writeEnum(out, patchType);
     writeString(out, oldName);
@@ -198,6 +208,7 @@
     writeBytes(out, header);
     writeVarInt32(out, insertions);
     writeVarInt32(out, deletions);
+    writeFixInt64(out, sizeDelta);
 
     writeVarInt32(out, edits.size());
     for (final Edit e : edits) {
@@ -208,17 +219,18 @@
     }
   }
 
-  static PatchListEntry readFrom(final InputStream in) throws IOException {
-    final ChangeType changeType = readEnum(in, ChangeType.values());
-    final PatchType patchType = readEnum(in, PatchType.values());
-    final String oldName = readString(in);
-    final String newName = readString(in);
-    final byte[] hdr = readBytes(in);
-    final int ins = readVarInt32(in);
-    final int del = readVarInt32(in);
+  static PatchListEntry readFrom(InputStream in) throws IOException {
+    ChangeType changeType = readEnum(in, ChangeType.values());
+    PatchType patchType = readEnum(in, PatchType.values());
+    String oldName = readString(in);
+    String newName = readString(in);
+    byte[] hdr = readBytes(in);
+    int ins = readVarInt32(in);
+    int del = readVarInt32(in);
+    long sizeDelta = readFixInt64(in);
 
-    final int editCount = readVarInt32(in);
-    final Edit[] editArray = new Edit[editCount];
+    int editCount = readVarInt32(in);
+    Edit[] editArray = new Edit[editCount];
     for (int i = 0; i < editCount; i++) {
       int beginA = readVarInt32(in);
       int endA = readVarInt32(in);
@@ -228,7 +240,7 @@
     }
 
     return new PatchListEntry(changeType, patchType, oldName, newName, hdr,
-        toList(editArray), ins, del);
+        toList(editArray), ins, del, sizeDelta);
   }
 
   private static List<Edit> toList(Edit[] l) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index b645e6c..6f957da 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -33,7 +33,7 @@
 import java.io.Serializable;
 
 public class PatchListKey implements Serializable {
-  static final long serialVersionUID = 17L;
+  static final long serialVersionUID = 18L;
 
   private transient ObjectId oldId;
   private transient ObjectId newId;
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 5cc8b87..0e421db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,6 +15,9 @@
 
 package com.google.gerrit.server.patch;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
 import com.google.common.base.Function;
 import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
@@ -59,6 +62,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.slf4j.Logger;
@@ -93,6 +97,7 @@
   private final PatchListKey key;
   private final Project.NameKey project;
   private final long timeoutMillis;
+  private final Object lock;
 
   @AssistedInject
   PatchListLoader(GitRepositoryManager mgr,
@@ -107,6 +112,7 @@
     diffExecutor = de;
     key = k;
     project = p;
+    lock = new Object();
     timeoutMillis =
         ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.FILE_NAME,
             "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
@@ -191,11 +197,16 @@
       entries.add(newCommitMessage(cmp, reader,
           againstParent ? null : aCommit, b));
       for (int i = 0; i < cnt; i++) {
-        DiffEntry diffEntry = diffEntries.get(i);
-        if (paths == null || paths.contains(diffEntry.getNewPath())
-            || paths.contains(diffEntry.getOldPath())) {
-          FileHeader fh = toFileHeader(key, df, diffEntry);
-          entries.add(newEntry(aTree, fh));
+        DiffEntry e = diffEntries.get(i);
+        if (paths == null || paths.contains(e.getNewPath())
+            || paths.contains(e.getOldPath())) {
+
+          FileHeader fh = toFileHeader(key, df, e);
+          long oldSize =
+              getFileSize(repo, reader, e.getOldMode(), e.getOldPath(), aTree);
+          long newSize =
+              getFileSize(repo, reader, e.getNewMode(), e.getNewPath(), bTree);
+          entries.add(newEntry(aTree, fh, newSize - oldSize));
         }
       }
       return new PatchList(a, b, againstParent,
@@ -203,6 +214,23 @@
     }
   }
 
+  private static long getFileSize(Repository repo, ObjectReader reader,
+      FileMode mode, String path, RevTree t) throws IOException {
+    if (!isBlob(mode)) {
+      return 0;
+    }
+    try (TreeWalk tw = TreeWalk.forPath(reader, path, t)) {
+      return tw != null
+          ? repo.open(tw.getObjectId(0), OBJ_BLOB).getSize()
+          : 0;
+    }
+  }
+
+  private static boolean isBlob(FileMode mode) {
+    int t = mode.getBits() & FileMode.TYPE_MASK;
+    return t == FileMode.TYPE_FILE || t == FileMode.TYPE_SYMLINK;
+  }
+
   private FileHeader toFileHeader(PatchListKey key,
       final DiffFormatter diffFormatter, final DiffEntry diffEntry)
       throws IOException {
@@ -210,7 +238,9 @@
     Future<FileHeader> result = diffExecutor.submit(new Callable<FileHeader>() {
       @Override
       public FileHeader call() throws IOException {
-        return diffFormatter.toFileHeader(diffEntry);
+        synchronized (lock) {
+          return diffFormatter.toFileHeader(diffEntry);
+        }
       }
     });
 
@@ -224,7 +254,9 @@
                       + " comparing " + diffEntry.getOldId().name()
                       + ".." + diffEntry.getNewId().name());
       result.cancel(true);
-      return toFileHeaderWithoutMyersDiff(diffFormatter, diffEntry);
+      synchronized (lock) {
+        return toFileHeaderWithoutMyersDiff(diffFormatter, diffEntry);
+      }
     } catch (ExecutionException e) {
       // If there was an error computing the result, carry it
       // up to the caller so the cache knows this key is invalid.
@@ -266,33 +298,40 @@
         aCommit != null ? Text.forCommit(reader, aCommit) : Text.EMPTY;
     Text bText = Text.forCommit(reader, bCommit);
 
-    byte[] rawHdr = hdr.toString().getBytes("UTF-8");
-    RawText aRawText = new RawText(aText.getContent());
-    RawText bRawText = new RawText(bText.getContent());
+    byte[] rawHdr = hdr.toString().getBytes(UTF_8);
+    byte[] aContent = aText.getContent();
+    byte[] bContent = bText.getContent();
+    long sizeDelta = bContent.length - aContent.length;
+    RawText aRawText = new RawText(aContent);
+    RawText bRawText = new RawText(bContent);
     EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
     FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
-    return new PatchListEntry(fh, edits);
+    return new PatchListEntry(fh, edits, sizeDelta);
   }
 
-  private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader) {
+  private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader,
+      long sizeDelta) {
     final FileMode oldMode = fileHeader.getOldMode();
     final FileMode newMode = fileHeader.getNewMode();
 
     if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
+          sizeDelta);
     }
 
     if (aTree == null // want combined diff
         || fileHeader.getPatchType() != PatchType.UNIFIED
         || fileHeader.getHunks().isEmpty()) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
+          sizeDelta);
     }
 
     List<Edit> edits = fileHeader.toEditList();
     if (edits.isEmpty()) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
+          sizeDelta);
     } else {
-      return new PatchListEntry(fileHeader, edits);
+      return new PatchListEntry(fileHeader, edits, sizeDelta);
     }
   }
 
@@ -393,7 +432,7 @@
           MergeResult<? extends Sequence> p = entry.getValue();
           try (TemporaryBuffer buf =
               new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
-            fmt.formatMerge(buf, p, "BASE", oursName, theirsName, "UTF-8");
+            fmt.formatMerge(buf, p, "BASE", oursName, theirsName, UTF_8.name());
             buf.close();
 
             try (InputStream in = buf.openInputStream()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 802a837..2169671 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.edit.ChangeEdit;
@@ -314,9 +313,9 @@
           break;
       }
 
-      final CurrentUser user = control.getCurrentUser();
+      final CurrentUser user = control.getUser();
       if (user.isIdentifiedUser()) {
-        final Account.Id me = ((IdentifiedUser) user).getAccountId();
+        final Account.Id me = user.getAccountId();
         switch (changeType) {
           case ADDED:
           case MODIFIED:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 83856db..16598b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -56,7 +56,9 @@
     this.byEmailCache = byEmailCache;
   }
 
-  public PatchSetInfo get(RevCommit src, PatchSet.Id psi) {
+  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi)
+      throws IOException {
+    rw.parseBody(src);
     PatchSetInfo info = new PatchSetInfo(psi);
     info.setSubject(src.getShortMessage());
     info.setMessage(src.getFullMessage());
@@ -83,7 +85,7 @@
         RevWalk rw = new RevWalk(repo)) {
       final RevCommit src =
           rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-      PatchSetInfo info = get(src, patchSet.getId());
+      PatchSetInfo info = get(rw, src, patchSet.getId());
       info.setParents(toParentInfos(src.getParents(), rw));
       return info;
     } catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
index 882e25f..7982479 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.patch;
 
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -37,7 +40,6 @@
 
 public class Text extends RawText {
   private static final Logger log = LoggerFactory.getLogger(Text.class);
-  private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
   private static final int bigFileThreshold = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
 
   public static final byte[] NO_BYTES = {};
@@ -81,7 +83,7 @@
       appendPersonIdent(b, "Commit", c.getCommitterIdent());
       b.append("\n");
       b.append(c.getFullMessage());
-      return new Text(b.toString().getBytes("UTF-8"));
+      return new Text(b.toString().getBytes(UTF_8));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index 6eb336d..d94df9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -102,19 +102,19 @@
         throw new InvalidPluginException("Cannot auto-register", err);
       } catch (RuntimeException err) {
         PluginLoader.log.warn(String.format(
-            "Plugin %s has invaild class file %s inside of %s", pluginName,
+            "Plugin %s has invalid class file %s inside of %s", pluginName,
             entry.getName(), jarFile.getName()), err);
         continue;
       }
 
-      if (def.isConcrete()) {
-        if (!Strings.isNullOrEmpty(def.annotationName)) {
-          rawMap.put(def.annotationName, def);
+      if (!Strings.isNullOrEmpty(def.annotationName)) {
+        if (def.isConcrete()) {
+            rawMap.put(def.annotationName, def);
+        } else {
+          PluginLoader.log.warn(String.format(
+              "Plugin %s tries to @%s(\"%s\") abstract class %s", pluginName,
+              def.annotationName, def.annotationValue, def.className));
         }
-      } else {
-        PluginLoader.log.warn(String.format(
-            "Plugin %s tries to @%s(\"%s\") abstract class %s", pluginName,
-            def.annotationName, def.annotationValue, def.className));
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
index ef153ce..f0c2b78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -60,7 +60,7 @@
   @Override
   public BanResultInfo apply(ProjectResource rsrc, Input input)
       throws UnprocessableEntityException, AuthException,
-      ResourceConflictException, IOException, InterruptedException {
+      ResourceConflictException, IOException {
     BanResultInfo r = new BanResultInfo();
     if (input != null && input.commits != null && !input.commits.isEmpty()) {
       List<ObjectId> commitsToBan = new ArrayList<>(input.commits.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 83db726..3ab6ff5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
@@ -27,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -124,7 +125,7 @@
   }
 
   public ChangeControl forUser(final CurrentUser who) {
-    if (getCurrentUser().equals(who)) {
+    if (getUser().equals(who)) {
       return this;
     }
     return new ChangeControl(changeDataFactory,
@@ -135,8 +136,8 @@
     return refControl;
   }
 
-  public CurrentUser getCurrentUser() {
-    return getRefControl().getCurrentUser();
+  public CurrentUser getUser() {
+    return getRefControl().getUser();
   }
 
   public ProjectControl getProjectControl() {
@@ -177,12 +178,23 @@
     return isVisible(db);
   }
 
+  /** Can this user see the given patchset? */
+  public boolean isPatchVisible(PatchSet ps, ChangeData cd)
+      throws OrmException {
+    checkArgument(cd.getId().equals(ps.getId().getParentKey()),
+        "%s not for change %s", ps, cd.getId());
+    if (ps.isDraft() && !isDraftVisible(cd.db(), cd)) {
+      return false;
+    }
+    return isVisible(cd.db());
+  }
+
   /** Can this user abandon this change? */
   public boolean canAbandon() {
     return isOwner() // owner (aka creator) of the change can abandon
         || getRefControl().isOwner() // branch owner can abandon
         || getProjectControl().isOwner() // project owner can abandon
-        || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+        || getUser().getCapabilities().canAdministrateServer() // site administers are god
         || getRefControl().canAbandon() // user can abandon a specific ref
     ;
   }
@@ -252,9 +264,9 @@
 
   /** Is this user the owner of the change? */
   public boolean isOwner() {
-    if (getCurrentUser().isIdentifiedUser()) {
-      final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
-      return i.getAccountId().equals(getChange().getOwner());
+    if (getUser().isIdentifiedUser()) {
+      Account.Id id = getUser().asIdentifiedUser().getAccountId();
+      return id.equals(getChange().getOwner());
     }
     return false;
   }
@@ -267,10 +279,9 @@
   /** Is this user a reviewer for the change? */
   public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
       throws OrmException {
-    if (getCurrentUser().isIdentifiedUser()) {
+    if (getUser().isIdentifiedUser()) {
       Collection<Account.Id> results = changeData(db, cd).reviewers().values();
-      IdentifiedUser user = (IdentifiedUser) getCurrentUser();
-      return results.contains(user.getAccountId());
+      return results.contains(getUser().getAccountId());
     }
     return false;
   }
@@ -284,9 +295,8 @@
     if (getChange().getStatus().isOpen()) {
       // A user can always remove themselves.
       //
-      if (getCurrentUser().isIdentifiedUser()) {
-        final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
-        if (i.getAccountId().equals(reviewer)) {
+      if (getUser().isIdentifiedUser()) {
+        if (getUser().getAccountId().equals(reviewer)) {
           return true; // can remove self
         }
       }
@@ -302,7 +312,7 @@
       if (getRefControl().canRemoveReviewer() // has removal permissions
           || getRefControl().isOwner() // branch owner
           || getProjectControl().isOwner() // project owner
-          || getCurrentUser().getCapabilities().canAdministrateServer()) {
+          || getUser().getCapabilities().canAdministrateServer()) {
         return true;
       }
     }
@@ -316,7 +326,7 @@
       return isOwner() // owner (aka creator) of the change can edit topic
           || getRefControl().isOwner() // branch owner can edit topic
           || getProjectControl().isOwner() // project owner can edit topic
-          || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+          || getUser().getCapabilities().canAdministrateServer() // site administers are god
           || getRefControl().canEditTopicName() // user can edit topic on a specific ref
       ;
     } else {
@@ -329,7 +339,7 @@
     return isOwner() // owner (aka creator) of the change can edit hashtags
           || getRefControl().isOwner() // branch owner can edit hashtags
           || getProjectControl().isOwner() // project owner can edit hashtags
-          || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+          || getUser().getCapabilities().canAdministrateServer() // site administers are god
           || getRefControl().canEditHashtags(); // user can edit hashtag on a specific ref
   }
 
@@ -343,7 +353,7 @@
 
   private boolean match(String destBranch, String refPattern) {
     return RefPatternMatcher.getMatcher(refPattern).match(destBranch,
-        this.getRefControl().getCurrentUser().getUserName());
+        getUser().getUserName());
   }
 
   private ChangeData changeData(ReviewDb db, @Nullable ChangeData cd) {
@@ -352,6 +362,7 @@
 
   public boolean isDraftVisible(ReviewDb db, ChangeData cd)
       throws OrmException {
-    return isOwner() || isReviewer(db, cd) || getRefControl().canViewDrafts();
+    return isOwner() || isReviewer(db, cd) || getRefControl().canViewDrafts()
+        || getUser().isInternalUser();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index ae8fd53..6cbf7c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -46,6 +46,7 @@
   public InheritedBooleanInfo createNewChangeForAllNotInTarget;
   public InheritedBooleanInfo requireChangeId;
   public InheritedBooleanInfo enableSignedPush;
+  public InheritedBooleanInfo requireSignedPush;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
   public com.google.gerrit.extensions.client.ProjectState state;
@@ -74,6 +75,7 @@
     InheritedBooleanInfo createNewChangeForAllNotInTarget =
         new InheritedBooleanInfo();
     InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
+    InheritedBooleanInfo requireSignedPush = new InheritedBooleanInfo();
 
     useContributorAgreements.value = projectState.isUseContributorAgreements();
     useSignedOffBy.value = projectState.isUseSignedOffBy();
@@ -90,6 +92,7 @@
     createNewChangeForAllNotInTarget.configuredValue =
         p.getCreateNewChangeForAllNotInTarget();
     enableSignedPush.configuredValue = p.getEnableSignedPush();
+    requireSignedPush.configuredValue = p.getRequireSignedPush();
 
     ProjectState parentState = Iterables.getFirst(projectState
         .parents(), null);
@@ -102,6 +105,7 @@
       createNewChangeForAllNotInTarget.inheritedValue =
           parentState.isCreateNewChangeForAllNotInTarget();
       enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
+      requireSignedPush.inheritedValue = projectState.isRequireSignedPush();
     }
 
     this.useContributorAgreements = useContributorAgreements;
@@ -111,6 +115,7 @@
     this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
     if (serverEnableSignedPush) {
       this.enableSignedPush = enableSignedPush;
+      this.requireSignedPush = requireSignedPush;
     }
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
@@ -138,7 +143,7 @@
     actions = Maps.newTreeMap();
     for (UiAction.Description d : UiActions.from(
         views, new ProjectResource(control),
-        Providers.of(control.getCurrentUser()))) {
+        Providers.of(control.getUser()))) {
       actions.put(d.getId(), new ActionInfo(d));
     }
     this.theme = projectState.getTheme();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index 40bbb12..4646e3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -100,7 +100,7 @@
       throw new ResourceNotFoundException(id);
     }
 
-    CurrentUser user = myCtl.getCurrentUser();
+    CurrentUser user = myCtl.getUser();
     String ref = parts.get(0);
     String path = parts.get(1);
     for (ProjectState ps : myCtl.getProjectState().tree()) {
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 a5a96b1..03dc97c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
@@ -100,7 +100,7 @@
         }
       }
     }.setContentType("text/plain")
-     .setCharacterEncoding(UTF_8.name())
+     .setCharacterEncoding(UTF_8)
      .disableGzip();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
index 8acc29e..09555b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
@@ -90,7 +90,7 @@
       if ("default".equals(id)) {
         throw new ResourceNotFoundException();
       } else if (!Strings.isNullOrEmpty(id)) {
-        ctl = ps.controlFor(ctl.getCurrentUser());
+        ctl = ps.controlFor(ctl.getUser());
         return parse(ctl, id);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
index 5b78e08..a94d17e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index f7914e9..a50705d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -14,8 +14,6 @@
 
 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.Sets;
@@ -35,9 +33,6 @@
 import com.google.inject.Inject;
 import com.google.inject.util.Providers;
 
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -50,7 +45,6 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -96,18 +90,15 @@
   @Override
   public List<BranchInfo> apply(ProjectResource rsrc)
       throws ResourceNotFoundException, IOException, BadRequestException {
-    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();
+    return new RefFilter<BranchInfo>(Constants.R_HEADS)
+        .subString(matchSubstring)
+        .regex(matchRegex)
+        .start(start)
+        .limit(limit)
+        .filter(allBranches(rsrc));
   }
 
-  private FluentIterable<BranchInfo> allBranches(ProjectResource rsrc)
+  private List<BranchInfo> allBranches(ProjectResource rsrc)
       throws IOException, ResourceNotFoundException {
     List<Ref> refs;
     try (Repository db = repoManager.openRepository(rsrc.getNameKey())) {
@@ -162,7 +153,7 @@
       }
     }
     Collections.sort(branches, new BranchComparator());
-    return FluentIterable.from(branches);
+    return branches;
   }
 
   private static class BranchComparator implements Comparator<BranchInfo> {
@@ -184,61 +175,6 @@
     }
   }
 
-  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();
@@ -249,7 +185,7 @@
     for (UiAction.Description d : UiActions.from(
         branchViews,
         new BranchResource(refControl.getProjectControl(), info),
-        Providers.of(refControl.getCurrentUser()))) {
+        Providers.of(refControl.getUser()))) {
       if (info.actions == null) {
         info.actions = new TreeMap<>();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
index 8d1e95f..b2596be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -58,7 +58,7 @@
   public List<ProjectInfo> apply(ProjectResource rsrc) {
     if (recursive) {
       return getChildProjectsRecursively(rsrc.getNameKey(),
-          rsrc.getControl().getCurrentUser());
+          rsrc.getControl().getUser());
     } else {
       return getDirectChildProjects(rsrc.getNameKey());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index 0208829..b4bd9a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -64,7 +64,7 @@
     List<List<DashboardInfo>> all = Lists.newArrayList();
     boolean setDefault = true;
     for (ProjectState ps : ctl.getProjectState().tree()) {
-      ctl = ps.controlFor(ctl.getCurrentUser());
+      ctl = ps.controlFor(ctl.getUser());
       if (ctl.isVisible()) {
         List<DashboardInfo> list = scan(ctl, project, setDefault);
         for (DashboardInfo d : list) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 04058f2..0704cb9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
@@ -37,9 +39,9 @@
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.StringUtil;
 import com.google.gerrit.server.WebLinks;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.util.RegexListSearcher;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
@@ -59,7 +61,6 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -109,7 +110,7 @@
 
   private final CurrentUser currentUser;
   private final ProjectCache projectCache;
-  private final GroupCache groupCache;
+  private final GroupsCollection groupsCollection;
   private final GroupControl.Factory groupControlFactory;
   private final GitRepositoryManager repoManager;
   private final ProjectNode.Factory projectNodeFactory;
@@ -191,13 +192,16 @@
   private AccountGroup.UUID groupUuid;
 
   @Inject
-  protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
-      GroupCache groupCache, GroupControl.Factory groupControlFactory,
-      GitRepositoryManager repoManager, ProjectNode.Factory projectNodeFactory,
+  protected ListProjects(CurrentUser currentUser,
+      ProjectCache projectCache,
+      GroupsCollection groupsCollection,
+      GroupControl.Factory groupControlFactory,
+      GitRepositoryManager repoManager,
+      ProjectNode.Factory projectNodeFactory,
       WebLinks webLinks) {
     this.currentUser = currentUser;
     this.projectCache = projectCache;
-    this.groupCache = groupCache;
+    this.groupsCollection = groupsCollection;
     this.groupControlFactory = groupControlFactory;
     this.repoManager = repoManager;
     this.projectNodeFactory = projectNodeFactory;
@@ -232,7 +236,7 @@
       display(buf);
       return BinaryResult.create(buf.toByteArray())
           .setContentType("text/plain")
-          .setCharacterEncoding("UTF-8");
+          .setCharacterEncoding(UTF_8);
     }
     return apply();
   }
@@ -246,12 +250,8 @@
       throws BadRequestException {
     PrintWriter stdout = null;
     if (displayOutputStream != null) {
-      try {
-        stdout = new PrintWriter(new BufferedWriter(
-            new OutputStreamWriter(displayOutputStream, "UTF-8")));
-      } catch (UnsupportedEncodingException e) {
-        throw new RuntimeException("JVM lacks UTF-8 encoding", e);
-      }
+      stdout = new PrintWriter(new BufferedWriter(
+          new OutputStreamWriter(displayOutputStream, UTF_8)));
     }
 
     int foundIndex = 0;
@@ -280,7 +280,7 @@
             break;
           }
           if (!pctl.getLocalGroups().contains(
-              GroupReference.forGroup(groupCache.get(groupUuid)))) {
+              GroupReference.forGroup(groupsCollection.parseId(groupUuid.get())))) {
             continue;
           }
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
index 1c140e3..4f157df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -16,7 +16,8 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -39,6 +39,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -46,13 +47,37 @@
 import java.util.List;
 import java.util.Map;
 
-@Singleton
 public class ListTags implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final Provider<ReviewDb> dbProvider;
   private final TagCache tagCache;
   private final ChangeCache changeCache;
 
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of tags to list")
+  public void setLimit(int limit) {
+    this.limit = limit;
+  }
+
+  @Option(name = "--start", aliases = {"-s"}, metaVar = "CNT", usage = "number of tags to skip")
+  public void setStart(int start) {
+    this.start = start;
+  }
+
+  @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match tags substring")
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
+
+  @Option(name = "--regex", aliases = {"-r"}, metaVar = "REGEX", usage = "match tags regex")
+  public void setMatchRegex(String matchRegex) {
+    this.matchRegex = matchRegex;
+  }
+
+  private int limit;
+  private int start;
+  private String matchSubstring;
+  private String matchRegex;
+
   @Inject
   public ListTags(GitRepositoryManager repoManager,
       Provider<ReviewDb> dbProvider,
@@ -66,19 +91,15 @@
 
   @Override
   public List<TagInfo> apply(ProjectResource resource) throws IOException,
-      ResourceNotFoundException {
+      ResourceNotFoundException, BadRequestException {
     List<TagInfo> tags = Lists.newArrayList();
 
-    try (Repository repo = getRepository(resource.getNameKey())) {
-      RevWalk rw = new RevWalk(repo);
-      try {
-        Map<String, Ref> all = visibleTags(resource.getControl(), repo,
-            repo.getRefDatabase().getRefs(Constants.R_TAGS));
-        for (Ref ref : all.values()) {
-          tags.add(createTagInfo(ref, rw));
-        }
-      } finally {
-        rw.dispose();
+    try (Repository repo = getRepository(resource.getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      Map<String, Ref> all = visibleTags(resource.getControl(), repo,
+          repo.getRefDatabase().getRefs(Constants.R_TAGS));
+      for (Ref ref : all.values()) {
+        tags.add(createTagInfo(ref, rw));
       }
     }
 
@@ -89,7 +110,12 @@
       }
     });
 
-    return tags;
+    return new RefFilter<TagInfo>(Constants.R_TAGS)
+        .start(start)
+        .limit(limit)
+        .subString(matchSubstring)
+        .regex(matchRegex)
+        .filter(tags);
   }
 
   public TagInfo get(ProjectResource resource, IdString id)
@@ -131,7 +157,7 @@
       RevTag tag = (RevTag)object;
       // Annotated or signed tag
       return new TagInfo(
-          Constants.R_TAGS + tag.getTagName(),
+          ref.getName(),
           tag.getName(),
           tag.getObject().getName(),
           tag.getFullMessage().trim(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 7f7dff6..9116dcc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -212,7 +211,7 @@
         public List<String> get() {
           List<String> r;
           if (user.isIdentifiedUser()) {
-            Set<String> emails = ((IdentifiedUser) user).getEmailAddresses();
+            Set<String> emails = user.asIdentifiedUser().getEmailAddresses();
             r = new ArrayList<>(emails.size() + 1);
             r.addAll(emails);
           } else {
@@ -232,7 +231,7 @@
     return ctl;
   }
 
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
@@ -258,7 +257,7 @@
 
   /** Can this user see this project exists? */
   public boolean isVisible() {
-    return (user instanceof InternalUser
+    return (user.isInternalUser()
         || canPerformOnAnyRef(Permission.READ)) && !isHidden();
   }
 
@@ -287,7 +286,7 @@
   }
 
   public boolean allRefsAreVisible(Set<String> ignore) {
-    return user instanceof InternalUser
+    return user.isInternalUser()
         || canPerformOnAllRefs(Permission.READ, ignore);
   }
 
@@ -350,7 +349,7 @@
     if (! (user.isIdentifiedUser())) {
       return new Capable("Must be logged in to verify Contributor Agreement");
     }
-    final IdentifiedUser iUser = (IdentifiedUser) user;
+    final IdentifiedUser iUser = user.asIdentifiedUser();
 
     List<AccountGroup.UUID> okGroupIds = Lists.newArrayList();
     for (ContributorAgreement ca : contributorAgreements) {
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 455a42b..7094828 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -413,6 +413,15 @@
     });
   }
 
+  public boolean isRequireSignedPush() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getRequireSignedPush();
+      }
+    });
+  }
+
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
     for (ProjectState s : treeInOrder()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index e529043..76ad2f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.EnableSignedPush;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -71,6 +70,7 @@
     public InheritableBoolean createNewChangeForAllNotInTarget;
     public InheritableBoolean requireChangeId;
     public InheritableBoolean enableSignedPush;
+    public InheritableBoolean requireSignedPush;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
     public com.google.gerrit.extensions.client.ProjectState state;
@@ -87,7 +87,7 @@
   private final PluginConfigFactory cfgFactory;
   private final AllProjectsNameProvider allProjects;
   private final DynamicMap<RestView<ProjectResource>> views;
-  private final Provider<CurrentUser> currentUser;
+  private final Provider<CurrentUser> user;
   private final ChangeHooks hooks;
 
   @Inject
@@ -102,7 +102,7 @@
       AllProjectsNameProvider allProjects,
       DynamicMap<RestView<ProjectResource>> views,
       ChangeHooks hooks,
-      Provider<CurrentUser> currentUser) {
+      Provider<CurrentUser> user) {
     this.serverEnableSignedPush = serverEnableSignedPush;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
@@ -114,7 +114,7 @@
     this.allProjects = allProjects;
     this.views = views;
     this.hooks = hooks;
-    this.currentUser = currentUser;
+    this.user = user;
   }
 
   @Override
@@ -167,8 +167,13 @@
         p.setRequireChangeID(input.requireChangeId);
       }
 
-      if (input.enableSignedPush != null) {
-        p.setEnableSignedPush(input.enableSignedPush);
+      if (serverEnableSignedPush) {
+        if (input.enableSignedPush != null) {
+          p.setEnableSignedPush(input.enableSignedPush);
+        }
+        if (input.requireSignedPush != null) {
+          p.setRequireSignedPush(input.requireSignedPush);
+        }
       }
 
       if (input.maxObjectSizeLimit != null) {
@@ -194,10 +199,9 @@
         ObjectId commitRev = projectConfig.commit(md);
         // Only fire hook if project was actually changed.
         if (!Objects.equals(baseRev, commitRev)) {
-          IdentifiedUser user = (IdentifiedUser) currentUser.get();
           hooks.doRefUpdatedHook(
             new Branch.NameKey(projectName, RefNames.REFS_CONFIG),
-            baseRev, commitRev, user.getAccount());
+            baseRev, commitRev, user.get().asIdentifiedUser().getAccount());
         }
         projectCache.evict(projectConfig.getProject());
         gitMgr.setProjectDescription(projectName, p.getDescription());
@@ -214,7 +218,7 @@
 
       ProjectState state = projectStateFactory.create(projectConfig);
       return new ConfigInfo(serverEnableSignedPush,
-          state.controlFor(currentUser.get()), config, pluginConfigEntries,
+          state.controlFor(user.get()), config, pluginConfigEntries,
           cfgFactory, allProjects, views);
     } catch (ConfigInvalidException err) {
       throw new ResourceConflictException("Cannot read project " + projectName, err);
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 1d7c724..d589865 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -67,7 +67,7 @@
     }
 
     ProjectControl ctl = resource.getControl();
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    IdentifiedUser user = ctl.getUser().asIdentifiedUser();
     if (!ctl.isOwner()) {
       throw new AuthException("not project owner");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index cfabf13..964554d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -26,8 +26,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.group.SystemGroupBackend;
 
 import dk.brics.automaton.RegExp;
@@ -50,6 +48,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 
 /** Manages access control for Git references (aka branches, tags). */
@@ -86,8 +86,8 @@
     return projectControl;
   }
 
-  public CurrentUser getCurrentUser() {
-    return projectControl.getCurrentUser();
+  public CurrentUser getUser() {
+    return projectControl.getUser();
   }
 
   public RefControl forUser(CurrentUser who) {
@@ -116,7 +116,7 @@
   public boolean isVisible() {
     if (isVisible == null) {
       isVisible =
-          (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
+          (getUser().isInternalUser() || canPerform(Permission.READ))
               && canRead();
     }
     return isVisible;
@@ -205,7 +205,7 @@
       // this why for the AllProjects project we allow administrators to push
       // configuration changes if they have push without being project owner.
       if (!(projectControl.getProjectState().isAllProjects() &&
-          getCurrentUser().getCapabilities().canAdministrateServer())) {
+          getUser().getCapabilities().canAdministrateServer())) {
         return false;
       }
     }
@@ -256,12 +256,12 @@
     }
     boolean owner;
     boolean admin;
-    switch (getCurrentUser().getAccessPath()) {
+    switch (getUser().getAccessPath()) {
       case REST_API:
       case JSON_RPC:
       case UNKNOWN:
         owner = isOwner();
-        admin = getCurrentUser().getCapabilities().canAdministrateServer();
+        admin = getUser().getCapabilities().canAdministrateServer();
         break;
 
       default:
@@ -301,10 +301,9 @@
       final PersonIdent tagger = tag.getTaggerIdent();
       if (tagger != null) {
         boolean valid;
-        if (getCurrentUser().isIdentifiedUser()) {
-          final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
+        if (getUser().isIdentifiedUser()) {
           final String addr = tagger.getEmailAddress();
-          valid = user.hasEmailAddress(addr);
+          valid = getUser().asIdentifiedUser().hasEmailAddress(addr);
         } else {
           valid = false;
         }
@@ -359,12 +358,12 @@
       return false;
     }
 
-    switch (getCurrentUser().getAccessPath()) {
+    switch (getUser().getAccessPath()) {
       case GIT:
         return canPushWithForce();
 
       default:
-        return getCurrentUser().getCapabilities().canAdministrateServer()
+        return getUser().getCapabilities().canAdministrateServer()
             || (isOwner() && !isForceBlocked(Permission.PUSH))
             || canPushWithForce();
     }
@@ -683,5 +682,10 @@
     } else if (!Repository.isValidRefName(refPattern)) {
       throw new InvalidNameException(refPattern);
     }
+    try {
+      Pattern.compile(refPattern.replace("${username}/", ""));
+    } catch (PatternSyntaxException e) {
+      throw new InvalidNameException(refPattern + " " + e.getMessage());
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java
new file mode 100644
index 0000000..63fb595
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
+import com.google.gerrit.extensions.api.projects.RefInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+import java.util.List;
+import java.util.Locale;
+
+public class RefFilter<T extends RefInfo> {
+  private final String prefix;
+  private String matchSubstring;
+  private String matchRegex;
+  private int start;
+  private int limit;
+
+  public RefFilter(String prefix) {
+    this.prefix = prefix;
+  }
+
+  public RefFilter<T> subString(String subString) {
+    this.matchSubstring = subString;
+    return this;
+  }
+
+  public RefFilter<T> regex(String regex) {
+    this.matchRegex = regex;
+    return this;
+  }
+
+  public RefFilter<T> start(int start) {
+    this.start = start;
+    return this;
+  }
+
+  public RefFilter<T> limit(int limit) {
+    this.limit = limit;
+    return this;
+  }
+
+  public List<T> filter(List<T> refs) throws BadRequestException {
+    FluentIterable<T> results = FluentIterable.from(refs);
+    if (!Strings.isNullOrEmpty(matchSubstring)) {
+      results = results.filter(new SubstringPredicate(matchSubstring));
+    } else if (!Strings.isNullOrEmpty(matchRegex)) {
+      results = results.filter(new RegexPredicate(matchRegex));
+    }
+    if (start > 0) {
+      results = results.skip(start);
+    }
+    if (limit > 0) {
+      results = results.limit(limit);
+    }
+    return results.toList();
+  }
+
+  private class SubstringPredicate implements Predicate<T> {
+    private final String substring;
+
+    private SubstringPredicate(String substring) {
+      this.substring = substring.toLowerCase(Locale.US);
+    }
+
+    @Override
+    public boolean apply(T in) {
+      String ref = in.ref;
+      if (ref.startsWith(prefix)) {
+        ref = ref.substring(prefix.length());
+      }
+      ref = ref.toLowerCase(Locale.US);
+      return ref.contains(substring);
+    }
+  }
+
+  private class RegexPredicate implements Predicate<T> {
+    private final RunAutomaton a;
+
+    private RegexPredicate(String regex) throws BadRequestException {
+      if (regex.startsWith("^")) {
+        regex = regex.substring(1);
+        if (regex.endsWith("$") && !regex.endsWith("\\$")) {
+          regex = regex.substring(0, regex.length() - 1);
+        }
+      }
+      try {
+        a = new RunAutomaton(new RegExp(regex).toAutomaton());
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
+
+    @Override
+    public boolean apply(T in) {
+      String ref = in.ref;
+      if (ref.startsWith(prefix)) {
+        ref = ref.substring(prefix.length());
+      }
+      return a.run(ref);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
index b18b8ec..ac01de5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
@@ -68,7 +67,6 @@
     input.id = Strings.emptyToNull(input.id);
 
     ProjectControl ctl = resource.getControl();
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
     if (!ctl.isOwner()) {
       throw new AuthException("not project owner");
     }
@@ -105,7 +103,7 @@
         if (!msg.endsWith("\n")) {
           msg += "\n";
         }
-        md.setAuthor(user);
+        md.setAuthor(ctl.getUser().asIdentifiedUser());
         md.setMessage(msg);
         config.commit(md);
         cache.evict(ctl.getProject());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 3b031ee..15c977f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -71,7 +71,6 @@
       ResourceNotFoundException, UnprocessableEntityException, IOException {
     ProjectControl ctl = rsrc.getControl();
     validateParentUpdate(ctl, input.parent, checkIfAdmin);
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
     try {
       MetaDataUpdate md = updateFactory.create(rsrc.getNameKey());
       try {
@@ -88,7 +87,7 @@
         } else if (!msg.endsWith("\n")) {
           msg += "\n";
         }
-        md.setAuthor(user);
+        md.setAuthor(ctl.getUser().asIdentifiedUser());
         md.setMessage(msg);
         config.commit(md);
         cache.evict(ctl.getProject());
@@ -109,7 +108,7 @@
   public void validateParentUpdate(final ProjectControl ctl, String newParent,
       boolean checkIfAdmin) throws AuthException, ResourceConflictException,
       UnprocessableEntityException {
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    IdentifiedUser user = ctl.getUser().asIdentifiedUser();
     if (checkIfAdmin && !user.getCapabilities().canAdministrateServer()) {
       throw new AuthException("not administrator");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index e8e29c1..2b35cef 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
@@ -220,7 +220,7 @@
     try {
       results = evaluateImpl("locate_submit_rule", "can_submit",
           "locate_submit_filter", "filter_submit_results",
-          control.getCurrentUser());
+          control.getUser());
     } catch (RuleEvalException e) {
       return ruleError(e.getMessage(), e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
index 12be5d3..afbd3be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 8061a26..9677f5f 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
@@ -544,7 +544,7 @@
   }
 
   void cacheVisibleTo(ChangeControl ctl) {
-    visibleTo = ctl.getCurrentUser();
+    visibleTo = ctl.getUser();
     changeControl = ctl;
   }
 
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 776a7f6..f52edc1 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
@@ -34,8 +34,8 @@
 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.account.VersionedAccountDestinations;
+import com.google.gerrit.server.account.VersionedAccountQueries;
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -111,6 +111,7 @@
   public static final String FIELD_HASHTAG = "hashtag";
   public static final String FIELD_LABEL = "label";
   public static final String FIELD_LIMIT = "limit";
+  public static final String FIELD_MERGE = "merge";
   public static final String FIELD_MERGEABLE = "mergeable";
   public static final String FIELD_MESSAGE = "message";
   public static final String FIELD_OWNER = "owner";
@@ -262,8 +263,7 @@
     Arguments asUser(Account.Id otherId) {
       try {
         CurrentUser u = self.get();
-        if (u.isIdentifiedUser()
-            && otherId.equals(((IdentifiedUser) u).getAccountId())) {
+        if (u.isIdentifiedUser() && otherId.equals(u.getAccountId())) {
           return this;
         }
       } catch (ProvisionException e) {
@@ -274,9 +274,9 @@
 
     IdentifiedUser getIdentifiedUser() throws QueryParseException {
       try {
-        CurrentUser u = getCurrentUser();
+        CurrentUser u = getUser();
         if (u.isIdentifiedUser()) {
-          return (IdentifiedUser) u;
+          return u.asIdentifiedUser();
         }
         throw new QueryParseException(NotSignedInException.MESSAGE);
       } catch (ProvisionException e) {
@@ -284,7 +284,7 @@
       }
     }
 
-    CurrentUser getCurrentUser() throws QueryParseException {
+    CurrentUser getUser() throws QueryParseException {
       try {
         return self.get();
       } catch (ProvisionException e) {
@@ -611,11 +611,7 @@
     Account.Id callerId;
     try {
       CurrentUser caller = args.self.get();
-      if (caller.isIdentifiedUser()) {
-        callerId = ((IdentifiedUser) caller).getAccountId();
-      } else {
-        callerId = null;
-      }
+      callerId = caller.isIdentifiedUser() ? caller.getAccountId() : null;
     } catch (ProvisionException e) {
       callerId = null;
     }
@@ -678,7 +674,7 @@
   }
 
   public Predicate<ChangeData> is_visible() throws QueryParseException {
-    return visibleto(args.getCurrentUser());
+    return visibleto(args.getUser());
   }
 
   @Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 8bea099..ea8a3ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -42,8 +43,12 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -64,8 +69,7 @@
     for (final Change c : changes) {
       final ChangeDataCache changeDataCache = new ChangeDataCache(
           c, db, args.changeDataFactory, args.projectCache);
-      List<String> files = args.changeDataFactory.create(db.get(), c)
-          .currentFilePaths();
+      List<String> files = listFiles(c, args, changeDataCache);
       List<Predicate<ChangeData>> filePredicates =
           Lists.newArrayListWithCapacity(files.size());
       for (String file : files) {
@@ -81,7 +85,10 @@
           new ProjectPredicate(c.getProject().get()));
       predicatesForOneChange.add(
           new RefPredicate(c.getDest().get()));
-      predicatesForOneChange.add(or(filePredicates));
+
+      predicatesForOneChange.add(or(or(filePredicates),
+          new IsMergePredicate(args, value)));
+
       predicatesForOneChange.add(new OperatorPredicate<ChangeData>(
           ChangeQueryBuilder.FIELD_CONFLICTS, value) {
 
@@ -109,17 +116,15 @@
           }
           try (Repository repo =
                 args.repoManager.openRepository(otherChange.getProject());
-              RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+              CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
             RevFlag canMergeFlag = rw.newFlag("CAN_MERGE");
             CodeReviewCommit commit =
-                (CodeReviewCommit) rw.parseCommit(changeDataCache.getTestAgainst());
-            SubmitStrategy strategy =
-                args.submitStrategyFactory.create(submitType,
-                    db.get(), repo, rw, null, canMergeFlag,
-                    getAlreadyAccepted(repo, rw, commit),
-                    otherChange.getDest(), null);
-            CodeReviewCommit otherCommit =
-                (CodeReviewCommit) rw.parseCommit(other);
+                rw.parseCommit(changeDataCache.getTestAgainst());
+            SubmitStrategy strategy = args.submitStrategyFactory.create(
+                submitType, db.get(), repo, rw, null, canMergeFlag,
+                getAlreadyAccepted(repo, rw, commit), otherChange.getDest(),
+                null);
+            CodeReviewCommit otherCommit = rw.parseCommit(other);
             otherCommit.add(canMergeFlag);
             conflicts = !strategy.dryRun(commit, otherCommit);
             args.conflictsCache.put(conflictsKey, conflicts);
@@ -171,6 +176,42 @@
     return changePredicates;
   }
 
+  private static List<String> listFiles(Change c, Arguments args,
+      ChangeDataCache changeDataCache) throws OrmException {
+    try (Repository repo = args.repoManager.openRepository(c.getProject());
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit ps = rw.parseCommit(changeDataCache.getTestAgainst());
+      if (ps.getParentCount() > 1) {
+        String dest = c.getDest().get();
+        Ref destBranch = repo.getRefDatabase().getRef(dest);
+        destBranch.getObjectId();
+        rw.setRevFilter(RevFilter.MERGE_BASE);
+        rw.markStart(rw.parseCommit(destBranch.getObjectId()));
+        rw.markStart(ps);
+        RevCommit base = rw.next();
+        // TODO(zivkov): handle the case with multiple merge bases
+
+        List<String> files = new ArrayList<>();
+        try (TreeWalk tw = new TreeWalk(repo)) {
+          if (base != null) {
+            tw.setFilter(TreeFilter.ANY_DIFF);
+            tw.addTree(base.getTree());
+          }
+          tw.addTree(ps.getTree());
+          tw.setRecursive(true);
+          while (tw.next()) {
+            files.add(tw.getPathString());
+          }
+        }
+        return files;
+      } else {
+        return args.changeDataFactory.create(args.db.get(), c).currentFilePaths();
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
   @Override
   public String toString() {
     return ChangeQueryBuilder.FIELD_CONFLICTS + ":" + value;
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 10c3db3..aa977dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -14,20 +14,21 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.server.index.ChangeField.SUBMISSIONID;
 import static com.google.gerrit.server.query.Predicate.and;
 import static com.google.gerrit.server.query.Predicate.not;
 import static com.google.gerrit.server.query.Predicate.or;
 import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Iterables;
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexConfig;
@@ -38,10 +39,15 @@
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Execute a single query over changes, for use by Gerrit internals.
@@ -76,14 +82,17 @@
   private final IndexConfig indexConfig;
   private final QueryProcessor qp;
   private final IndexCollection indexes;
+  private final ChangeData.Factory changeDataFactory;
 
   @Inject
   InternalChangeQuery(IndexConfig indexConfig,
       QueryProcessor queryProcessor,
-      IndexCollection indexes) {
+      IndexCollection indexes,
+      ChangeData.Factory changeDataFactory) {
     this.indexConfig = indexConfig;
     qp = queryProcessor.enforceVisibility(false);
     this.indexes = indexes;
+    this.changeDataFactory = changeDataFactory;
   }
 
   public InternalChangeQuery setLimit(int n) {
@@ -125,39 +134,62 @@
         open()));
   }
 
-  public Iterable<ChangeData> byCommitsOnBranchNotMerged(Branch.NameKey branch,
-      List<String> hashes) throws OrmException {
-    Schema<ChangeData> schema = schema(indexes);
-    if (schema != null && schema.hasField(ChangeField.EXACT_COMMIT)) {
-      return query(commitsOnBranchNotMerged(branch, commits(schema, hashes)));
-    } else {
-      return byCommitsOnBranchNotMerged(
-          schema, branch, hashes, indexConfig.maxPrefixTerms());
-    }
+  public Iterable<ChangeData> byCommitsOnBranchNotMerged(Repository repo,
+      ReviewDb db, Branch.NameKey branch, List<String> hashes)
+      throws OrmException, IOException {
+    return byCommitsOnBranchNotMerged(repo, db, branch, hashes,
+        // Account for all commit predicates plus ref, project, status.
+        indexConfig.maxTerms() - 3);
   }
 
   @VisibleForTesting
-  Iterable<ChangeData> byCommitsOnBranchNotMerged(Schema<ChangeData> schema,
-      Branch.NameKey branch, List<String> hashes, int batchSize)
-      throws OrmException {
-    List<Predicate<ChangeData>> commits = commits(schema, hashes);
-    int numBatches = (hashes.size() / batchSize) + 1;
-    List<Predicate<ChangeData>> queries = new ArrayList<>(numBatches);
-    for (List<Predicate<ChangeData>> batch
-        : Iterables.partition(commits, batchSize)) {
-      queries.add(commitsOnBranchNotMerged(branch, batch));
+  Iterable<ChangeData> byCommitsOnBranchNotMerged(Repository repo, ReviewDb db,
+      Branch.NameKey branch, List<String> hashes, int indexLimit)
+      throws OrmException, IOException {
+    if (hashes.size() > indexLimit) {
+      return byCommitsOnBranchNotMergedFromDatabase(repo, db, branch, hashes);
+    } else {
+      return byCommitsOnBranchNotMergedFromIndex(branch, hashes);
     }
-    try {
-      return FluentIterable.from(qp.queryChanges(queries))
-        .transformAndConcat(new Function<QueryResult, List<ChangeData>>() {
-          @Override
-          public List<ChangeData> apply(QueryResult in) {
-            return in.changes();
-          }
-        });
-    } catch (QueryParseException e) {
-      throw new OrmException(e);
+  }
+
+  private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase(
+      Repository repo, ReviewDb db, Branch.NameKey branch, List<String> hashes)
+      throws OrmException, IOException {
+    Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size());
+    String lastPrefix = null;
+    for (Ref ref :
+        repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES).values()) {
+      String r = ref.getName();
+      if ((lastPrefix != null && r.startsWith(lastPrefix))
+          || !hashes.contains(ref.getObjectId().name())) {
+        continue;
+      }
+      Change.Id id = Change.Id.fromRef(r);
+      if (id == null) {
+        continue;
+      }
+      if (changeIds.add(id)) {
+        lastPrefix = r.substring(0, r.lastIndexOf('/'));
+      }
     }
+
+    List<ChangeData> cds = new ArrayList<>(hashes.size());
+    for (Change c : db.changes().get(changeIds)) {
+      if (c.getDest().equals(branch) && c.getStatus() != Change.Status.MERGED) {
+        cds.add(changeDataFactory.create(db, c));
+      }
+    }
+    return cds;
+  }
+
+  private Iterable<ChangeData> byCommitsOnBranchNotMergedFromIndex(
+      Branch.NameKey branch, List<String> hashes) throws OrmException {
+    return query(and(
+        ref(branch),
+        project(branch.getParentKey()),
+        not(status(Change.Status.MERGED)),
+        or(commits(schema(indexes), hashes))));
   }
 
   private static List<Predicate<ChangeData>> commits(Schema<ChangeData> schema,
@@ -169,15 +201,6 @@
     return commits;
   }
 
-  private static Predicate<ChangeData> commitsOnBranchNotMerged(
-      Branch.NameKey branch, List<Predicate<ChangeData>> commits) {
-    return and(
-        ref(branch),
-        project(branch.getParentKey()),
-        not(status(Change.Status.MERGED)),
-        or(commits));
-  }
-
   public List<ChangeData> byProjectOpen(Project.NameKey project)
       throws OrmException {
     return query(and(project(project), open()));
@@ -192,6 +215,14 @@
     return query(commit(schema(indexes), id.name()));
   }
 
+  public List<ChangeData> bySubmissionId(String cs) throws OrmException {
+    if (Strings.isNullOrEmpty(cs) || !schema(indexes).hasField(SUBMISSIONID)) {
+      return Collections.emptyList();
+    } else {
+      return query(new SubmissionIdPredicate(cs));
+    }
+  }
+
   public List<ChangeData> byProjectGroups(Project.NameKey project,
       Collection<String> groups) throws OrmException {
     List<GroupPredicate> groupPredicates = new ArrayList<>(groups.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
new file mode 100644
index 0000000..3c02bab
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+public class IsMergePredicate extends OperatorPredicate<ChangeData> {
+  private final Arguments args;
+
+  public IsMergePredicate(Arguments args, String value) {
+    super(ChangeQueryBuilder.FIELD_MERGE, value);
+    this.args = args;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    ObjectId id = ObjectId.fromString(
+        cd.currentPatchSet().getRevision().get());
+    try (Repository repo =
+          args.repoManager.openRepository(cd.change().getProject());
+        RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(id);
+      return commit.getParentCount() > 1;
+    } catch (IOException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 2;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index b990091..1ac2729 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -33,7 +33,7 @@
     ChangeDataSource {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
-      return ((IdentifiedUser) user).getAccountId().toString();
+      return user.getAccountId().toString();
     }
     return user.toString();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index dc7a579..856a559 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.OperatorPredicate;
@@ -27,7 +26,7 @@
 class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
-      return ((IdentifiedUser) user).getAccountId().toString();
+      return user.getAccountId().toString();
     }
     if (user instanceof SingleGroupUser) {
       return "group:" + user.getEffectiveGroups().getKnownGroups() //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 606f577..44e0654 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -18,7 +18,6 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
@@ -29,7 +28,7 @@
 class IsWatchedByPredicate extends AndPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
-      return ((IdentifiedUser) user).getAccountId().toString();
+      return user.getAccountId().toString();
     }
     return user.toString();
   }
@@ -39,13 +38,13 @@
   IsWatchedByPredicate(ChangeQueryBuilder.Arguments args,
       boolean checkIsVisible) throws QueryParseException {
     super(filters(args, checkIsVisible));
-    this.user = args.getCurrentUser();
+    this.user = args.getUser();
   }
 
   private static List<Predicate<ChangeData>> filters(
       ChangeQueryBuilder.Arguments args,
       boolean checkIsVisible) throws QueryParseException {
-    CurrentUser user = args.getCurrentUser();
+    CurrentUser user = args.getUser();
     List<Predicate<ChangeData>> r = Lists.newArrayList();
     ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
     for (AccountProjectWatch w : user.getNotificationFilters()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index f73e0e4..b84cc79 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -164,7 +166,7 @@
   public void query(String queryString) throws IOException {
     out = new PrintWriter( //
         new BufferedWriter( //
-            new OutputStreamWriter(outputStream, "UTF-8")));
+            new OutputStreamWriter(outputStream, UTF_8)));
     try {
       if (queryProcessor.isDisabled()) {
         ErrorMessage m = new ErrorMessage();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index bd6b297..dfc0f75e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -99,7 +99,8 @@
       out = query();
     } catch (QueryParseException e) {
       // This is a hack to detect an operator that requires authentication.
-      Pattern p = Pattern.compile("^Error in operator (.*:self)$");
+      Pattern p = Pattern.compile(
+          "^Error in operator (.*:self|is:watched|is:owner|is:reviewer|has:.*)$");
       Matcher m = p.matcher(e.getMessage());
       if (m.matches()) {
         String op = m.group(1);
@@ -126,7 +127,7 @@
     IdentifiedUser self = null;
     try {
       if (user.get().isIdentifiedUser()) {
-        self = (IdentifiedUser) user.get();
+        self = user.get().asIdentifiedUser();
         self.asyncStarredChanges();
       }
       return query0();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
new file mode 100644
index 0000000..3b2dd94
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.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.Change;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+class SubmissionIdPredicate extends IndexPredicate<ChangeData> {
+
+  SubmissionIdPredicate(String changeSet) {
+    super(ChangeField.SUBMISSIONID, changeSet);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    Change change = object.change();
+    if (change == null) {
+      return false;
+    }
+    if (change.getSubmissionId() == null) {
+      return false;
+    }
+    return getValue().equals(change.getSubmissionId());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
index f50f123..777b5b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
@@ -22,6 +22,7 @@
   @Override
   protected void configure() {
     bind(DataSourceType.class).annotatedWith(Names.named("db2")).to(DB2.class);
+    bind(DataSourceType.class).annotatedWith(Names.named("derby")).to(Derby.class);
     bind(DataSourceType.class).annotatedWith(Names.named("h2")).to(H2.class);
     bind(DataSourceType.class).annotatedWith(Names.named("jdbc")).to(JDBC.class);
     bind(DataSourceType.class).annotatedWith(Names.named("mysql")).to(MySql.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Derby.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Derby.java
new file mode 100644
index 0000000..dd300c6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Derby.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.schema;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+class Derby extends BaseDataSourceType {
+
+  protected final Config cfg;
+  private final SitePaths site;
+
+  @Inject
+  Derby(@GerritServerConfig Config cfg,
+      SitePaths site) {
+    super("org.apache.derby.jdbc.EmbeddedDriver");
+    this.cfg = cfg;
+    this.site = site;
+  }
+
+  @Override
+  public String getUrl() {
+    String database = cfg.getString("database", null, "database");
+    if (database == null || database.isEmpty()) {
+      database = "db/ReviewDB";
+    }
+    return "jdbc:derby:" + site.resolve(database).toString() + ";create=true";
+  }
+}
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 769329d..a4a7ddc 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_111> C = Schema_111.class;
+  public static final Class<Schema_113> C = Schema_113.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
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
index 8cbf119..cfafb37 100644
--- 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
@@ -19,6 +19,7 @@
 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.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -143,9 +144,15 @@
     SetMultimap<Project.NameKey, Change.Id> openByProject =
         HashMultimap.create();
     for (Change c : db.changes().all()) {
-      if (c.getStatus().isOpen()) {
-        openByProject.put(c.getProject(), c.getId());
+      Status status = c.getStatus();
+      if (status != null && status.isClosed()) {
+        continue;
       }
+
+      // The old "submitted" state is not supported anymore
+      // (thus status is null) but it was an opened state and needs
+      // to be migrated as such
+      openByProject.put(c.getProject(), c.getId());
     }
     return openByProject;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
similarity index 65%
copy from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
index cd01186..3e879bd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.server.schema;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_112 extends SchemaVersion {
+  @Inject
+  Schema_112(Provider<Schema_111> prior) {
+    super(prior);
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_113.java
similarity index 65%
copy from gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_113.java
index cd01186..32d655e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_113.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.server.schema;
 
-/** Detail necessary to display an action. */
-public class UiCommandDetail {
-  public String id;
-  public String method;
-  public String label;
-  public String title;
-  public boolean enabled;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_113 extends SchemaVersion {
+  @Inject
+  Schema_113(Provider<Schema_112> prior) {
+    super(prior);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
index 8ef32c0..684a72e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.schema;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.CharMatcher;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -73,7 +75,7 @@
   }
 
   private List<String> parse(final InputStream in) throws IOException {
-    try (BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"))) {
+    try (BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8))) {
       String delimiter = ";";
       List<String> commands = new ArrayList<>();
       StringBuilder buffer = new StringBuilder();
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 7665c64..a2693e0 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,7 +26,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -36,8 +35,7 @@
 
   @Inject
   DefaultSecureStore(SitePaths site) {
-    Path secureConfig = site.etc_dir.resolve("secure.config");
-    sec = new FileBasedConfig(secureConfig.toFile(), FS.DETECTED);
+    sec = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
     try {
       sec.load();
     } catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
index 21634ac..f59bba9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.tools;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.common.Version;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -72,7 +74,7 @@
     SortedMap<String, Entry> toc = new TreeMap<>();
     final BufferedReader br =
         new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
-            read("TOC")), "UTF-8"));
+            read("TOC")), UTF_8));
     String line;
     while ((line = br.readLine()) != null) {
       if (line.length() > 0 && !line.startsWith("#")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
index 5514ef5..55c2992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
@@ -37,7 +37,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
index ee74b35..900bb42 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
@@ -40,7 +40,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginLogFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginLogFile.java
new file mode 100644
index 0000000..17f6535
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginLogFile.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.inject.Inject;
+
+import org.apache.log4j.AsyncAppender;
+import org.apache.log4j.Layout;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+public abstract class PluginLogFile implements LifecycleListener {
+
+  private final SystemLog systemLog;
+  private final ServerInformation serverInfo;
+  private final String logName;
+  private final Layout layout;
+
+  @Inject
+  public PluginLogFile(SystemLog systemLog,
+      ServerInformation serverInfo,
+      String logName,
+      Layout layout) {
+    this.systemLog = systemLog;
+    this.serverInfo = serverInfo;
+    this.logName = logName;
+    this.layout = layout;
+  }
+
+  @Override
+  public void start() {
+    AsyncAppender asyncAppender =
+        systemLog.createAsyncAppender(logName, layout);
+    Logger logger = LogManager.getLogger(logName);
+    logger.removeAppender(logName);
+    logger.addAppender(asyncAppender);
+    logger.setAdditivity(false);
+  }
+
+  @Override
+  public void stop() {
+    // stop is called when plugin is unloaded or when the server shutdown.
+    // Only clean up when the server is shutting down to prevent issue when a
+    // plugin is reloaded. The issue is that gerrit load the new plugin and then
+    // unload the old one so because loggers are static, the unload of the old
+    // plugin would remove the appenders just created by the new plugin.
+    if (serverInfo.getState() == ServerInformation.State.SHUTDOWN) {
+      LogManager.getLogger(logName).removeAllAppenders();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
index a836fd7..943e518 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
@@ -29,7 +29,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
index 86c74e0..506a1c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
@@ -23,6 +23,6 @@
  * by the GerritGlobalModule scope.
  */
 public interface RequestContext {
-  CurrentUser getCurrentUser();
+  CurrentUser getUser();
   Provider<ReviewDb> getReviewDbProvider();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index f63da5439..d1cf47c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -188,8 +188,8 @@
       public T call() throws Exception {
         RequestContext old = local.setContext(new RequestContext() {
           @Override
-          public CurrentUser getCurrentUser() {
-            return context.getCurrentUser();
+          public CurrentUser getUser() {
+            return context.getUser();
           }
 
           @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
index 2b6b86e..ede3365 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
@@ -31,7 +31,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
index 32cdca5..c857c40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.util;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -60,7 +62,7 @@
     final DailyRollingFileAppender dst = new DailyRollingFileAppender();
     dst.setName(name);
     dst.setLayout(layout);
-    dst.setEncoding("UTF-8");
+    dst.setEncoding(UTF_8.name());
     dst.setFile(resolve(logdir).resolve(name).toString());
     dst.setImmediateFlush(true);
     dst.setAppend(true);
@@ -72,6 +74,7 @@
 
   public AsyncAppender createAsyncAppender(String name, Layout layout) {
     AsyncAppender async = new AsyncAppender();
+    async.setName(name);
     async.setBlocking(true);
     async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
     async.setLocationInfo(false);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index d1b1da4..3e405a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -53,13 +53,13 @@
 
       @Provides
       CurrentUser provideCurrentUser(RequestContext ctx) {
-        return ctx.getCurrentUser();
+        return ctx.getUser();
       }
 
       @Provides
       IdentifiedUser provideCurrentUser(CurrentUser user) {
         if (user.isIdentifiedUser()) {
-          return (IdentifiedUser) user;
+          return user.asIdentifiedUser();
         }
         throw new ProvisionException(NotSignedInException.MESSAGE,
             new NotSignedInException());
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index a63b1e7..a3e1a96 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 
 import com.googlecode.prolog_cafe.exceptions.EvaluationException;
@@ -54,7 +53,7 @@
     Term resultTerm;
 
     if (curUser.isIdentifiedUser()) {
-      Account.Id id = ((IdentifiedUser)curUser).getAccountId();
+      Account.Id id = curUser.getAccountId();
       resultTerm = new IntegerTerm(id.get());
     } else if (curUser instanceof AnonymousUser) {
       resultTerm = anonymous;
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 2a45819..9a4e77c 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -283,6 +283,7 @@
 %% - The maximum is never used.
 %%
 any_with_block(Label, Min, reject(Who)) :-
+  Min < 0,
   check_label_range_permission(Label, Min, ok(Who)),
   !
   .
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 2646fd0..d93aa34 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
@@ -48,5 +48,6 @@
 v = text/x-verilog
 vert = x-shader/x-vertex
 vh = text/x-verilog
+vhdl = text/x-vhdl
 vm = text/velocity
 yaml = text/x-yaml
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
index e658729..8956e8f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.TimeUtil;
 import com.google.inject.Guice;
@@ -106,7 +107,7 @@
       SymbolTerm pathTerm = SymbolTerm.create(prologResource);
       JavaObjectTerm inTerm =
           new JavaObjectTerm(new PushbackReader(new BufferedReader(
-              new InputStreamReader(in, "UTF-8")), Prolog.PUSHBACK_SIZE));
+              new InputStreamReader(in, UTF_8)), Prolog.PUSHBACK_SIZE));
       if (!env.execute(Prolog.BUILTIN, "consult_stream", pathTerm, inTerm)) {
         throw new CompileException("Cannot consult " + prologResource);
       }
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 e953cbf..1e752d4 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
@@ -204,7 +204,7 @@
 
       @Provides
       @Singleton
-      CurrentUser getCurrentUser(IdentifiedUser.GenericFactory userFactory) {
+      CurrentUser getUser(IdentifiedUser.GenericFactory userFactory) {
         return userFactory.create(ownerId);
       }
     };
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 6100ffd..50a6c11 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
@@ -28,7 +28,7 @@
 
   @Test
   public void nullCommitMessage() throws Exception {
-    assertThat(HashtagsUtil.extractTags(null)).isEmpty();
+    assertThat(HashtagsUtil.extractTags((String) null)).isEmpty();
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
index 480efb4..ab00ba8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.common.truth.Truth.assertThat;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -21,11 +22,98 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
 
+import com.google.gerrit.extensions.client.Theme;
+
+import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
 import java.util.concurrent.TimeUnit;
 
 public class ConfigUtilTest {
+  private static final String SECT = "foo";
+  private static final String SUB = "bar";
+
+  static class SectionInfo {
+    public static final String CONSTANT = "42";
+    public transient String missing;
+    public int i;
+    public Integer ii;
+    public Integer id;
+    public long l;
+    public Long ll;
+    public Long ld;
+    public boolean b;
+    public Boolean bb;
+    public Boolean bd;
+    public String s;
+    public String sd;
+    public Theme t;
+    public Theme td;
+    static SectionInfo defaults() {
+      SectionInfo i = new SectionInfo();
+      i.i = 1;
+      i.ii = 2;
+      i.id = 3;
+      i.l = 4L;
+      i.ll = 5L;
+      i.ld = 6L;
+      i.b = true;
+      i.bb = false;
+      i.bd = true;
+      i.s = "foo";
+      i.sd = "bar";
+      i.t = Theme.DEFAULT;
+      i.td = Theme.DEFAULT;
+      return i;
+    }
+  }
+
+  @Test
+  public void testStoreLoadSection() throws Exception {
+    SectionInfo d = SectionInfo.defaults();
+    SectionInfo in = new SectionInfo();
+    in.missing = "42";
+    in.i = 1;
+    in.ii = 43;
+    in.l = 4L;
+    in.ll = -43L;
+    in.b = false;
+    in.bb = true;
+    in.bd = false;
+    in.s = "baz";
+    in.t = Theme.MIDNIGHT;
+
+    Config cfg = new Config();
+    ConfigUtil.storeSection(cfg, SECT, SUB, in, d);
+
+    assertThat(cfg.getString(SECT, SUB, "CONSTANT")).isNull();
+    assertThat(cfg.getString(SECT, SUB, "missing")).isNull();
+    assertThat(cfg.getBoolean(SECT, SUB, "b", false)).isEqualTo(in.b);
+    assertThat(cfg.getBoolean(SECT, SUB, "bb", false)).isEqualTo(in.bb);
+    assertThat(cfg.getInt(SECT, SUB, "i", 0)).isEqualTo(0);
+    assertThat(cfg.getInt(SECT, SUB, "ii", 0)).isEqualTo(in.ii);
+    assertThat(cfg.getLong(SECT, SUB, "l", 0L)).isEqualTo(0L);
+    assertThat(cfg.getLong(SECT, SUB, "ll", 0L)).isEqualTo(in.ll);
+    assertThat(cfg.getString(SECT, SUB, "s")).isEqualTo(in.s);
+    assertThat(cfg.getString(SECT, SUB, "sd")).isNull();
+
+    SectionInfo out = new SectionInfo();
+    ConfigUtil.loadSection(cfg, SECT, SUB, out, d);
+    assertThat(out.i).isEqualTo(in.i);
+    assertThat(out.ii).isEqualTo(in.ii);
+    assertThat(out.id).isEqualTo(d.id);
+    assertThat(out.l).isEqualTo(in.l);
+    assertThat(out.ll).isEqualTo(in.ll);
+    assertThat(out.ld).isEqualTo(d.ld);
+    assertThat(out.b).isEqualTo(in.b);
+    assertThat(out.bb).isEqualTo(in.bb);
+    assertThat(out.bd).isNull();
+    assertThat(out.s).isEqualTo(in.s);
+    assertThat(out.sd).isEqualTo(d.sd);
+    assertThat(out.t).isEqualTo(in.t);
+    assertThat(out.td).isEqualTo(d.td);
+  }
+
   @Test
   public void testTimeUnit() {
     assertEquals(ms(0, MILLISECONDS), parse("0"));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
index ac6d805..128c2a7 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
@@ -34,13 +34,18 @@
 import com.google.gerrit.server.query.change.OrSource;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
 
 public class IndexRewriteTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   private FakeIndex index;
   private IndexCollection indexes;
   private ChangeQueryBuilder queryBuilder;
@@ -52,7 +57,8 @@
     indexes = new IndexCollection();
     indexes.setSearchIndex(index);
     queryBuilder = new FakeQueryBuilder(indexes);
-    rewrite = new IndexRewriteImpl(indexes);
+    rewrite = new IndexRewriteImpl(indexes,
+        IndexConfig.create(0, 0, 3, 100));
   }
 
   @Test
@@ -209,6 +215,17 @@
         out.getChildren());
   }
 
+  @Test
+  public void testTooManyTerms() throws Exception {
+    String q = "file:a OR file:b OR file:c";
+    Predicate<ChangeData> in = parse(q);
+    assertEquals(query(in), rewrite(in));
+
+    exception.expect(QueryParseException.class);
+    exception.expectMessage("too many terms in query");
+    rewrite(parse(q + " OR file:d"));
+  }
+
   private Predicate<ChangeData> parse(String query) throws QueryParseException {
     return queryBuilder.parse(query);
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
index c7b81a2..145042c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
@@ -20,8 +20,6 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
-import java.io.UnsupportedEncodingException;
-
 public class AddressTest {
   @Rule
   public ExpectedException exception = ExpectedException.none();
@@ -155,10 +153,6 @@
   }
 
   private static String format(final String name, final String email) {
-    try {
-      return new Address(name, email).toHeaderString();
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("Cannot encode address", e);
-    }
+    return new Address(name, email).toHeaderString();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 85272d0..9706feb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -670,4 +671,29 @@
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
   }
+
+  @Test
+  public void testValidateRefPatternsOK() throws Exception {
+    RefControl.validateRefPattern("refs/*");
+    RefControl.validateRefPattern("^refs/heads/*");
+    RefControl.validateRefPattern("^refs/tags/[0-9a-zA-Z-_.]+");
+    RefControl.validateRefPattern("refs/heads/review/${username}/*");
+  }
+
+  @Test(expected = InvalidNameException.class)
+  public void testValidateBadRefPatternDoubleCaret() throws Exception {
+    RefControl.validateRefPattern("^^refs/*");
+  }
+
+  @Test(expected = InvalidNameException.class)
+  public void testValidateBadRefPatternDanglingCharacter() throws Exception {
+    RefControl
+        .validateRefPattern("^refs/heads/tmp/sdk/[0-9]{3,3}_R[1-9][A-Z][0-9]{3,3}*");
+  }
+
+  @Test
+  public void testValidateRefPatternNoDanglingCharacter() throws Exception {
+    RefControl
+        .validateRefPattern("^refs/heads/tmp/sdk/[0-9]{3,3}_R[1-9][A-Z][0-9]{3,3}");
+  }
 }
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 499caa2..1d79400 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
@@ -53,11 +53,13 @@
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -73,6 +75,7 @@
 
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeUtils;
@@ -113,6 +116,7 @@
 
   @ConfigSuite.Parameter public Config config;
   @Inject protected AccountManager accountManager;
+  @Inject protected BatchUpdate.Factory updateFactory;
   @Inject protected ChangeInserter.Factory changeFactory;
   @Inject protected PatchSetInserter.Factory patchSetFactory;
   @Inject protected ChangeControl.GenericFactory changeControlFactory;
@@ -163,7 +167,7 @@
         userFactory.create(Providers.of(db), requestUserId);
     return new RequestContext() {
       @Override
-      public CurrentUser getCurrentUser() {
+      public CurrentUser getUser() {
         return requestUser;
       }
 
@@ -210,8 +214,8 @@
   @Test
   public void byId() 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();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
     assertQuery("12345");
     assertQuery(change1.getId().get(), change1);
@@ -221,7 +225,7 @@
   @Test
   public void byKey() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change = newChange(repo, null, null, null, null).insert();
+    Change change = insert(newChange(repo, null, null, null, null));
     String key = change.getKey().get();
 
     assertQuery("I0000000000000000000000000000000000000000");
@@ -234,7 +238,7 @@
   @Test
   public void byTriplet() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change = newChange(repo, null, null, null, "branch").insert();
+    Change change = insert(newChange(repo, null, null, null, "branch"));
     String k = change.getKey().get();
 
     assertQuery("repo~branch~" + k, change);
@@ -260,11 +264,11 @@
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.MERGED);
-    ins2.insert();
+    insert(ins2);
 
     assertQuery("status:new", change1);
     assertQuery("status:NEW", change1);
@@ -279,15 +283,15 @@
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.DRAFT);
-    ins2.insert();
+    insert(ins2);
     ChangeInserter ins3 = newChange(repo, null, null, null, null);
     Change change3 = ins3.getChange();
     change3.setStatus(Change.Status.MERGED);
-    ins3.insert();
+    insert(ins3);
 
     Change[] expected = new Change[] {change2, change1};
     assertQuery("status:open", expected);
@@ -309,15 +313,15 @@
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.MERGED);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.ABANDONED);
-    ins2.insert();
+    insert(ins2);
     ChangeInserter ins3 = newChange(repo, null, null, null, null);
     Change change3 = ins3.getChange();
     change3.setStatus(Change.Status.NEW);
-    ins3.insert();
+    insert(ins3);
 
     Change[] expected = new Change[] {change2, change1};
     assertQuery("status:closed", expected);
@@ -337,11 +341,11 @@
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.MERGED);
-    ins2.insert();
+    insert(ins2);
 
     assertQuery("status:n", change1);
     assertQuery("status:ne", change1);
@@ -357,7 +361,7 @@
   public void byCommit() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
-    ins.insert();
+    insert(ins);
     String sha = ins.getPatchSet().getRevision().get();
 
     assertQuery("0000000000000000000000000000000000000000");
@@ -370,10 +374,10 @@
   @Test
   public void byOwner() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
-    Change change2 = newChange(repo, null, null, user2, null).insert();
+    Change change2 = insert(newChange(repo, null, null, user2, null));
 
     assertQuery("owner:" + userId.get(), change1);
     assertQuery("owner:" + user2, change2);
@@ -382,7 +386,7 @@
   @Test
   public void byAuthor() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
 
     // By exact email address
     assertQuery("author:jauthor@example.com", change1);
@@ -405,7 +409,7 @@
   @Test
   public void byCommitter() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
 
     // By exact email address
     assertQuery("committer:jcommitter@example.com", change1);
@@ -428,10 +432,10 @@
   @Test
   public void byOwnerIn() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
-    Change change2 = newChange(repo, null, null, user2, null).insert();
+    Change change2 = insert(newChange(repo, null, null, user2, null));
 
     assertQuery("ownerin:Administrators", change1);
     assertQuery("ownerin:\"Registered Users\"", change2, change1);
@@ -441,8 +445,8 @@
   public void byProject() throws Exception {
     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();
+    Change change1 = insert(newChange(repo1, null, null, null, null));
+    Change change2 = insert(newChange(repo2, null, null, null, null));
 
     assertQuery("project:foo");
     assertQuery("project:repo");
@@ -454,8 +458,8 @@
   public void byProjectPrefix() throws Exception {
     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();
+    Change change1 = insert(newChange(repo1, null, null, null, null));
+    Change change2 = insert(newChange(repo2, null, null, null, null));
 
     assertQuery("projects:foo");
     assertQuery("projects:repo1", change1);
@@ -466,8 +470,8 @@
   @Test
   public void byBranchAndRef() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, "master").insert();
-    Change change2 = newChange(repo, null, null, null, "branch").insert();
+    Change change1 = insert(newChange(repo, null, null, null, "master"));
+    Change change2 = insert(newChange(repo, null, null, null, "branch"));
 
     assertQuery("branch:foo");
     assertQuery("branch:master", change1);
@@ -487,24 +491,24 @@
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setTopic("feature1");
-    ins1.insert();
+    insert(ins1);
 
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setTopic("feature2");
-    ins2.insert();
+    insert(ins2);
 
     ChangeInserter ins3 = newChange(repo, null, null, null, null);
     Change change3 = ins3.getChange();
     change3.setTopic("Cherrypick-feature2");
-    ins3.insert();
+    insert(ins3);
 
     ChangeInserter ins4 = newChange(repo, null, null, null, null);
     Change change4 = ins4.getChange();
     change4.setTopic("feature2-fixup");
-    ins4.insert();
+    insert(ins4);
 
-    Change change5 = newChange(repo, null, null, null, null).insert();
+    Change change5 = insert(newChange(repo, null, null, null, null));
 
     assertQuery("intopic:foo");
     assertQuery("intopic:feature1", change1);
@@ -520,9 +524,9 @@
   public void byMessageExact() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
     RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
     assertQuery("message:foo");
     assertQuery("message:one", change1);
@@ -534,10 +538,10 @@
     TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 =
         repo.parseBody(repo.commit().message("12345 67890").create());
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
     RevCommit commit2 =
         repo.parseBody(repo.commit().message("12346 67891").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
     assertQuery("message:1234");
     assertQuery("message:12345", change1);
@@ -549,7 +553,7 @@
     accountManager.authenticate(AuthRequest.forUser("anotheruser"));
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
-    Change change = ins.insert();
+    Change change = insert(ins);
 
     gApi.changes().id(change.getId().get()).current()
       .review(new ReviewInput().label("Code-Review", 1));
@@ -591,7 +595,7 @@
     Change last = null;
     int n = 5;
     for (int i = 0; i < n; i++) {
-      last = newChange(repo, null, null, null, null).insert();
+      last = insert(newChange(repo, null, null, null, null));
     }
 
     for (int i = 1; i <= n + 2; i++) {
@@ -618,7 +622,7 @@
     TestRepository<Repo> repo = createProject("repo");
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 2; i++) {
-      changes.add(newChange(repo, null, null, null, null).insert());
+      changes.add(insert(newChange(repo, null, null, null, null)));
     }
 
     assertQuery("status:new", changes.get(1), changes.get(0));
@@ -632,7 +636,7 @@
     TestRepository<Repo> repo = createProject("repo");
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 3; i++) {
-      changes.add(newChange(repo, null, null, null, null).insert());
+      changes.add(insert(newChange(repo, null, null, null, null)));
     }
 
     assertQuery("status:new limit:2", changes.get(2), changes.get(1));
@@ -646,7 +650,7 @@
   @Test
   public void maxPages() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change = newChange(repo, null, null, null, null).insert();
+    Change change = insert(newChange(repo, null, null, null, null));
 
     QueryRequest query = newQuery("status:new").withLimit(10);
     assertQuery(query, change);
@@ -664,7 +668,7 @@
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 5; i++) {
       inserters.add(newChange(repo, null, null, null, null));
-      changes.add(inserters.get(i).insert());
+      changes.add(insert(inserters.get(i)));
     }
 
     for (int i : ImmutableList.of(2, 0, 1, 4, 3)) {
@@ -686,8 +690,8 @@
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
-    Change change1 = ins1.insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(ins1);
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
     assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2));
     assertQuery("status:new", change2, change1);
@@ -708,8 +712,8 @@
   public void updatedOrderWithSubMinuteResolution() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
-    Change change1 = ins1.insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(ins1);
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
     assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2));
 
@@ -730,11 +734,11 @@
   @Test
   public void filterOutMoreThanOnePageOfResults() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change = newChange(repo, null, null, userId.get(), null).insert();
+    Change change = insert(newChange(repo, null, null, userId.get(), null));
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     for (int i = 0; i < 5; i++) {
-      newChange(repo, null, null, user2, null).insert();
+      insert(newChange(repo, null, null, user2, null));
     }
 
     assertQuery("status:new ownerin:Administrators", change);
@@ -747,7 +751,7 @@
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     for (int i = 0; i < 5; i++) {
-      newChange(repo, null, null, user2, null).insert();
+      insert(newChange(repo, null, null, user2, null));
     }
 
     assertQuery("status:new ownerin:Administrators");
@@ -761,7 +765,7 @@
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
     assertQuery("file:file");
     assertQuery("file:dir", change);
@@ -778,7 +782,7 @@
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
     assertQuery("file:.*file.*");
     assertQuery("file:^file.*"); // Whole path only.
@@ -792,7 +796,7 @@
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
     assertQuery("path:file");
     assertQuery("path:dir");
@@ -809,7 +813,7 @@
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
     assertQuery("path:.*file.*");
     assertQuery("path:^dir.file.*", change);
@@ -819,7 +823,7 @@
   public void byComment() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
-    Change change = ins.insert();
+    Change change = insert(ins);
 
     ReviewInput input = new ReviewInput();
     input.message = "toplevel";
@@ -840,8 +844,8 @@
     long thirtyHours = MILLISECONDS.convert(30, HOURS);
     clockStepMs = thirtyHours;
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
     clockStepMs = 0; // Queried by AgePredicate constructor.
     long now = TimeUtil.nowMs();
     assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1))
@@ -862,8 +866,8 @@
   public void byBefore() throws Exception {
     clockStepMs = MILLISECONDS.convert(30, HOURS);
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
     clockStepMs = 0;
 
     assertQuery("before:2009-09-29");
@@ -882,8 +886,8 @@
   public void byAfter() throws Exception {
     clockStepMs = MILLISECONDS.convert(30, HOURS);
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
     clockStepMs = 0;
 
     assertQuery("after:2009-10-03");
@@ -904,8 +908,8 @@
     RevCommit commit2 = repo.parseBody(
         repo.commit().parent(commit1).add("file1", "foo").create());
 
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
     assertQuery("added:>4");
     assertQuery("-added:<=4");
@@ -954,8 +958,8 @@
 
   private List<Change> setUpHashtagChanges() 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();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
     HashtagsInput in = new HashtagsInput();
     in.add = ImmutableSet.of("foo");
@@ -997,20 +1001,20 @@
   public void byDefault() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
 
-    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
 
     RevCommit commit2 = repo.parseBody(
         repo.commit().message("foosubject").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
     RevCommit commit3 = repo.parseBody(
         repo.commit()
         .add("Foo.java", "foo contents")
         .create());
-    Change change3 = newChange(repo, commit3, null, null, null).insert();
+    Change change3 = insert(newChange(repo, commit3, null, null, null));
 
     ChangeInserter ins4 = newChange(repo, null, null, null, null);
-    Change change4 = ins4.insert();
+    Change change4 = insert(ins4);
     ReviewInput ri4 = new ReviewInput();
     ri4.message = "toplevel";
     ri4.labels = ImmutableMap.<String, Short> of("Code-Review", (short) 1);
@@ -1019,9 +1023,9 @@
     ChangeInserter ins5 = newChange(repo, null, null, null, null);
     Change change5 = ins5.getChange();
     change5.setTopic("feature5");
-    ins5.insert();
+    insert(ins5);
 
-    Change change6 = newChange(repo, null, null, null, "branch6").insert();
+    Change change6 = insert(newChange(repo, null, null, null, "branch6"));
 
     assertQuery(change1.getId().get(), change1);
     assertQuery(ChangeTriplet.format(change1), change1);
@@ -1042,11 +1046,11 @@
   @Test
   public void implicitVisibleTo() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.DRAFT);
-    ins2.insert();
+    insert(ins2);
 
     String q = "project:repo";
     assertQuery(q, change2, change1);
@@ -1060,11 +1064,11 @@
   @Test
   public void explicitVisibleTo() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.DRAFT);
-    ins2.insert();
+    insert(ins2);
 
     String q = "project:repo";
     assertQuery(q, change2, change1);
@@ -1079,8 +1083,8 @@
   @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();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
@@ -1105,12 +1109,12 @@
   @Test
   public void byFrom() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
 
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     ChangeInserter ins2 = newChange(repo, null, null, user2, null);
-    Change change2 = ins2.insert();
+    Change change2 = insert(ins2);
 
     ReviewInput input = new ReviewInput();
     input.message = "toplevel";
@@ -1146,10 +1150,10 @@
         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();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
+    Change change3 = insert(newChange(repo, commit3, null, null, null));
+    Change change4 = insert(newChange(repo, commit4, null, null, null));
 
     assertQuery("conflicts:" + change1.getId().get(), change3);
     assertQuery("conflicts:" + change2.getId().get());
@@ -1161,9 +1165,9 @@
   public void reviewedBy() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-    Change change3 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+    Change change3 = insert(newChange(repo, null, null, null, null));
 
     gApi.changes()
       .id(change1.getId().get())
@@ -1219,7 +1223,7 @@
     Branch.NameKey dest = null;
     for (int i = 0; i < n; i++) {
       ChangeInserter ins = newChange(repo, null, null, null, null);
-      ins.insert();
+      insert(ins);
       if (dest == null) {
         dest = ins.getChange().getDest();
       }
@@ -1229,7 +1233,7 @@
 
     for (int i = 1; i <= 11; i++) {
       Iterable<ChangeData> cds = internalChangeQuery.byCommitsOnBranchNotMerged(
-          indexes.getSearchIndex().getSchema(), dest, shas, i);
+          repo.getRepository(), db, dest, shas, i);
       Iterable<Integer> ids = FluentIterable.from(cds).transform(
           new Function<ChangeData, Integer>() {
             @Override
@@ -1237,7 +1241,7 @@
               return in.getId().get();
             }
           });
-      String name = "batch size " + i;
+      String name = "limit " + i;
       assertThat(ids).named(name).hasSize(n);
       assertThat(ids).named(name)
           .containsExactlyElementsIn(expectedIds);
@@ -1248,7 +1252,7 @@
   public void prepopulatedFields() throws Exception {
     assume().that(notesMigration.enabled()).isFalse();
     TestRepository<Repo> repo = createProject("repo");
-    Change change = newChange(repo, null, null, null, null).insert();
+    Change change = insert(newChange(repo, null, null, null, null));
 
     db = new DisabledReviewDb();
     requestContext.setContext(newRequestContext(userId));
@@ -1309,10 +1313,20 @@
     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, user),
-        change,
-        commit);
+    RefControl refControl = projectControlFactory.controlFor(project, user)
+        .controlForRef(change.getDest());
+    return changeFactory.create(refControl, change, commit)
+        .setValidatePolicy(CommitValidators.Policy.NONE);
+  }
+
+  protected Change insert(ChangeInserter ins) throws Exception {
+    try (BatchUpdate bu = updateFactory.create(
+        db, ins.getChange().getProject(), ins.getUser(),
+        ins.getChange().getCreatedOn())) {
+      bu.insertChange(ins);
+      bu.execute();
+      return ins.getChange();
+    }
   }
 
   protected Change newPatchSet(TestRepository<Repo> repo, Change c)
@@ -1325,13 +1339,23 @@
             .message("message")
             .add("file" + n, "contents " + n)
             .create());
-    ChangeControl ctl = changeControlFactory.controlFor(c.getId(), user);
-    return patchSetFactory.create(
-          repo.getRepository(), repo.getRevWalk(), ctl, commit)
+    RefControl ctl = projectControlFactory.controlFor(c.getProject(), user)
+        .controlForRef(c.getDest());
+
+    PatchSetInserter inserter = patchSetFactory.create(
+          ctl, new PatchSet.Id(c.getId(), n), commit)
         .setSendMail(false)
         .setRunHooks(false)
-        .setValidatePolicy(ValidatePolicy.NONE)
-        .insert();
+        .setValidatePolicy(CommitValidators.Policy.NONE);
+    try (BatchUpdate bu = updateFactory.create(
+        db, c.getProject(), user, TimeUtil.nowTs());
+        ObjectInserter oi = repo.getRepository().newObjectInserter()) {
+      bu.setRepository(repo.getRepository(), repo.getRevWalk(), oi);
+      bu.addOp(c.getId(), inserter);
+      bu.execute();
+    }
+
+    return inserter.getChange();
   }
 
   protected void assertBadQuery(Object query) throws Exception {
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 6122d65..b503a13 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -38,10 +38,10 @@
     TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 =
         repo.parseBody(repo.commit().message("foo_bar_foo").create());
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
     RevCommit commit2 =
         repo.parseBody(repo.commit().message("one.two.three").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
     assertQuery("message:foo_ba");
     assertQuery("message:bar", change1);
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
index 9bdd795..7e7899b 100644
--- 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
@@ -82,9 +82,9 @@
   public void isReviewed() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-    Change change3 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+    Change change3 = insert(newChange(repo, null, null, null, null));
 
     gApi.changes()
       .id(change1.getId().get())
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index 69eecc3..8b5e85a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -104,7 +104,7 @@
       IdentifiedUser user) throws OrmException {
     ChangeControl ctl = EasyMock.createNiceMock(ChangeControl.class);
     expect(ctl.getChange()).andStubReturn(c);
-    expect(ctl.getCurrentUser()).andStubReturn(user);
+    expect(ctl.getUser()).andStubReturn(user);
     ChangeNotes notes = new ChangeNotes(repoManager, migration, allUsers, c)
         .load();
     expect(ctl.getNotes()).andStubReturn(notes);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index a84570b..5c897e4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
@@ -51,13 +53,13 @@
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicReference;
 
 public abstract class BaseCommand implements Command {
   private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
-  public static final String ENC = "UTF-8";
+  public static final Charset ENC = UTF_8;
 
   private static final int PRIVATE_STATUS = 1 << 30;
   static final int STATUS_CANCEL = PRIVATE_STATUS | 1;
@@ -87,7 +89,7 @@
   private WorkQueue.Executor executor;
 
   @Inject
-  private Provider<CurrentUser> userProvider;
+  private Provider<CurrentUser> user;
 
   @Inject
   private Provider<SshScope.Context> contextProvider;
@@ -276,7 +278,7 @@
     final TaskThunk tt = new TaskThunk(thunk);
 
     if (isAdminHighPriorityCommand()
-        && userProvider.get().getCapabilities().canAdministrateServer()) {
+        && user.get().getCapabilities().canAdministrateServer()) {
       // Admin commands should not block the main work threads (there
       // might be an interactive shell there), nor should they wait
       // for the main work threads.
@@ -309,14 +311,7 @@
 
   /** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
   protected static PrintWriter toPrintWriter(final OutputStream o) {
-    try {
-      return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, ENC)));
-    } catch (UnsupportedEncodingException e) {
-      // Our default encoding is required by the specifications for the
-      // runtime APIs, this should never, ever happen.
-      //
-      throw new RuntimeException("JVM lacks " + ENC + " encoding", e);
-    }
+    return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, ENC)));
   }
 
   private int handleError(final Throwable e) {
@@ -337,8 +332,8 @@
     if (!(e instanceof UnloggedFailure)) {
       final StringBuilder m = new StringBuilder();
       m.append("Internal server error");
-      if (userProvider.get().isIdentifiedUser()) {
-        final IdentifiedUser u = (IdentifiedUser) userProvider.get();
+      if (user.get().isIdentifiedUser()) {
+        final IdentifiedUser u = user.get().asIdentifiedUser();
         m.append(" (user ");
         m.append(u.getAccount().getUserName());
         m.append(" account ");
@@ -403,8 +398,8 @@
 
       StringBuilder m = new StringBuilder();
       m.append(context.getCommandLine());
-      if (userProvider.get().isIdentifiedUser()) {
-        IdentifiedUser u = (IdentifiedUser) userProvider.get();
+      if (user.get().isIdentifiedUser()) {
+        IdentifiedUser u = user.get().asIdentifiedUser();
         m.append(" (").append(u.getAccount().getUserName()).append(")");
       }
       this.taskName = m.toString();
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 1a6b2dd..3ce4545 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Preconditions;
@@ -98,7 +99,7 @@
   public boolean authenticate(String username, PublicKey suppliedKey,
       ServerSession session) {
     SshSession sd = session.getAttribute(SshSession.KEY);
-    Preconditions.checkState(sd.getCurrentUser() == null);
+    Preconditions.checkState(sd.getUser() == null);
     if (PeerDaemonUser.USER_NAME.equals(username)) {
       if (myHostKeys.contains(suppliedKey)
           || getPeerKeys().contains(suppliedKey)) {
@@ -193,7 +194,7 @@
           }
 
           try {
-            byte[] bin = Base64.decodeBase64(line.getBytes("ISO-8859-1"));
+            byte[] bin = Base64.decodeBase64(line.getBytes(ISO_8859_1));
             keys.add(new Buffer(bin).getRawPublicKey());
           } catch (RuntimeException | SshException e) {
             logBadKey(path, line, e);
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 4d6a790..3a8a1f5 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
@@ -203,7 +203,7 @@
 
   private LoggingEvent log(final String msg) {
     final SshSession sd = session.get();
-    final CurrentUser user = sd.getCurrentUser();
+    final CurrentUser user = sd.getUser();
 
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
@@ -224,7 +224,7 @@
     String accountId = "-";
 
     if (user != null && user.isIdentifiedUser()) {
-      IdentifiedUser u = (IdentifiedUser) user;
+      IdentifiedUser u = user.asIdentifiedUser();
       userName = u.getAccount().getUserName();
       accountId = "a/" + u.getAccountId().toString();
 
@@ -261,7 +261,7 @@
     } else {
       SshSession session = ctx.getSession();
       sessionId = IdGenerator.format(session.getSessionId());
-      currentUser = session.getCurrentUser();
+      currentUser = session.getUser();
       created = ctx.created;
     }
     auditService.dispatch(new SshAuditEvent(sessionId, currentUser,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index cd09cfa..e3455e3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -80,10 +80,10 @@
     }
 
     @Override
-    public CurrentUser getCurrentUser() {
-      final CurrentUser user = session.getCurrentUser();
+    public CurrentUser getUser() {
+      CurrentUser user = session.getUser();
       if (user != null && user.isIdentifiedUser()) {
-        IdentifiedUser identifiedUser = userFactory.create(((IdentifiedUser) user).getAccountId());
+        IdentifiedUser identifiedUser = userFactory.create(user.getAccountId());
         identifiedUser.setAccessPath(user.getAccessPath());
         return identifiedUser;
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index f055b2f..ff160e0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -60,7 +60,7 @@
   }
 
   /** Identity of the authenticated user account on the socket. */
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return identity;
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
index b658440..cca426d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -121,7 +121,7 @@
   public static boolean success(final String username, final ServerSession session,
       final SshScope sshScope, final SshLog sshLog,
       final SshSession sd, final CurrentUser user) {
-    if (sd.getCurrentUser() == null) {
+    if (sd.getUser() == null) {
       sd.authenticationSuccess(username, user);
 
       // If this is the first time we've authenticated this
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 77738d0..c417f0a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
@@ -102,7 +104,7 @@
       if (!msg.endsWith("\n")) {
         msg += "\n";
       }
-      err.write(msg.getBytes("UTF-8"));
+      err.write(msg.getBytes(UTF_8));
       err.flush();
       onExit(1);
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 647d28d..ec8b94d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -71,7 +71,7 @@
       printCommits(r.newlyBanned, "The following commits were banned");
       printCommits(r.alreadyBanned, "The following commits were already banned");
       printCommits(r.ignored, "The following ids do not represent commits and were ignored");
-    } catch (RestApiException | IOException | InterruptedException e) {
+    } catch (RestApiException | IOException e) {
       throw die(e);
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index aeb69d0..d20a879 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Function;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -33,7 +35,6 @@
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -82,14 +83,14 @@
     }
   }
 
-  private String readSshKey() throws UnsupportedEncodingException, IOException {
+  private String readSshKey() throws IOException {
     if (sshKey == null) {
       return null;
     }
     if ("-".equals(sshKey)) {
       sshKey = "";
       BufferedReader br =
-          new BufferedReader(new InputStreamReader(in, "UTF-8"));
+          new BufferedReader(new InputStreamReader(in, UTF_8));
       String line;
       while ((line = br.readLine()) != null) {
         sshKey += line + "\n";
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 05c1bfc..c6eaebb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -34,12 +34,11 @@
 
   @Override
   protected void configure() {
-    final CommandName git = Commands.named("git");
-    final CommandName gerrit = Commands.named("gerrit");
-    CommandName index = Commands.named(gerrit, "index");
-    final CommandName logging = Commands.named(gerrit, "logging");
-    final CommandName plugin = Commands.named(gerrit, "plugin");
-    final CommandName testSubmit = Commands.named(gerrit, "test-submit");
+    CommandName git = Commands.named("git");
+    CommandName gerrit = Commands.named("gerrit");
+    CommandName logging = Commands.named(gerrit, "logging");
+    CommandName plugin = Commands.named(gerrit, "plugin");
+    CommandName testSubmit = Commands.named(gerrit, "test-submit");
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
     command(gerrit, AproposCommand.class);
@@ -58,10 +57,6 @@
     command(gerrit, VersionCommand.class);
     command(gerrit, GarbageCollectionCommand.class);
 
-    command(index).toProvider(new DispatchCommandProvider(index));
-    command(index, IndexActivateCommand.class);
-    command(index, IndexStartCommand.class);
-
     command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
     command(plugin, PluginLsCommand.class);
     command(plugin, PluginEnableCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
new file mode 100644
index 0000000..3e7b293
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.sshd.CommandModule;
+import com.google.gerrit.sshd.CommandName;
+import com.google.gerrit.sshd.Commands;
+import com.google.gerrit.sshd.DispatchCommandProvider;
+
+public class IndexCommandsModule extends CommandModule {
+
+  @Override
+  protected void configure() {
+    CommandName gerrit = Commands.named("gerrit");
+    CommandName index = Commands.named(gerrit, "index");
+    command(index).toProvider(new DispatchCommandProvider(index));
+    command(index, IndexActivateCommand.class);
+    command(index, IndexStartCommand.class);
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index ad72b13..75072e8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -19,10 +19,12 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GetGroups;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupJson;
@@ -71,12 +73,13 @@
         final Provider<IdentifiedUser> identifiedUser,
         final IdentifiedUser.GenericFactory userFactory,
         final Provider<GetGroups> accountGetGroups,
-        final GroupJson json) {
+        final GroupJson json,
+        GroupBackend groupBackend) {
       super(groupCache, groupControlFactory, genericGroupControlFactory,
-          identifiedUser, userFactory, accountGetGroups, json);
+          identifiedUser, userFactory, accountGetGroups, json, groupBackend);
     }
 
-    void display(final PrintWriter out) throws OrmException {
+    void display(final PrintWriter out) throws OrmException, BadRequestException {
       final ColumnFormatter formatter = new ColumnFormatter(out, '\t');
       for (final GroupInfo info : get()) {
         formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a"));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index ff60410..6c07fae 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -32,7 +34,6 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
@@ -65,11 +66,10 @@
 
   @Inject
   QueryShell(final SchemaFactory<ReviewDb> dbFactory,
-      @Assisted final InputStream in, @Assisted final OutputStream out)
-          throws UnsupportedEncodingException {
+      @Assisted final InputStream in, @Assisted final OutputStream out) {
     this.dbFactory = dbFactory;
-    this.in = new BufferedReader(new InputStreamReader(in, "UTF-8"));
-    this.out = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
+    this.in = new BufferedReader(new InputStreamReader(in, UTF_8));
+    this.out = new PrintWriter(new OutputStreamWriter(out, UTF_8));
   }
 
   public void setOutputFormat(OutputFormat fmt) {
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 1ca4c8c..4570085 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.common.io.CharStreams;
@@ -50,7 +52,6 @@
 
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -249,8 +250,7 @@
   }
 
   private ReviewInput reviewFromJson() throws UnloggedFailure {
-    try (InputStreamReader r =
-          new InputStreamReader(in, StandardCharsets.UTF_8)) {
+    try (InputStreamReader r = new InputStreamReader(in, UTF_8)) {
       return OutputFormat.JSON.newGson().
           fromJson(CharStreams.toString(r), ReviewInput.class);
     } catch (IOException | JsonSyntaxException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index bdc4cef..194e65f9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -22,6 +22,8 @@
  */
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.tools.ToolsCatalog.Entry;
 import com.google.gerrit.sshd.BaseCommand;
@@ -188,7 +190,7 @@
       }
     }
 
-    out.write("E\n".getBytes("UTF-8"));
+    out.write("E\n".getBytes(UTF_8));
     out.flush();
     readAck();
   }
@@ -210,7 +212,7 @@
     buf.append(" ");
     buf.append(dir.getName());
     buf.append("\n");
-    out.write(buf.toString().getBytes("UTF-8"));
+    out.write(buf.toString().getBytes(UTF_8));
     out.flush();
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 974b5c6..a6b2810 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.EmailException;
@@ -226,7 +228,7 @@
       in.raw = new RawInput() {
         @Override
         public InputStream getInputStream() throws IOException {
-          return new ByteArrayInputStream(sshKey.getBytes("UTF-8"));
+          return new ByteArrayInputStream(sshKey.getBytes(UTF_8));
         }
 
         @Override
@@ -312,7 +314,7 @@
       if (idx >= 0) {
         StringBuilder sshKey = new StringBuilder();
         BufferedReader br =
-            new BufferedReader(new InputStreamReader(in, "UTF-8"));
+            new BufferedReader(new InputStreamReader(in, UTF_8));
         String line;
         while ((line = br.readLine()) != null) {
           sshKey.append(line)
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 108df96..8ac9887 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -179,9 +179,9 @@
       return "";
     }
 
-    final CurrentUser user = sd.getCurrentUser();
+    final CurrentUser user = sd.getUser();
     if (user != null && user.isIdentifiedUser()) {
-      IdentifiedUser u = (IdentifiedUser) user;
+      IdentifiedUser u = user.asIdentifiedUser();
 
       if (!numeric) {
         String name = u.getAccount().getUserName();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 2dcff16..3f7914e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.common.EventSource;
@@ -125,7 +126,7 @@
       if (!msg.endsWith("\n")) {
         msg += "\n";
       }
-      err.write(msg.getBytes("UTF-8"));
+      err.write(msg.getBytes(UTF_8));
       err.flush();
       onExit(1);
       return;
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 905d776..5e36318 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
@@ -45,6 +45,7 @@
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.mime.MimeUtil2Module;
@@ -64,6 +65,7 @@
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
+import com.google.gerrit.sshd.commands.IndexCommandsModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -116,6 +118,7 @@
   private GuiceFilter filter;
 
   private ServletContext servletContext;
+  private IndexType indexType;
 
   @Override
   public void doFilter(ServletRequest req, ServletResponse res,
@@ -165,6 +168,7 @@
       }
 
       cfgInjector = createCfgInjector();
+      initIndexType();
       config = cfgInjector.getInstance(
           Key.get(Config.class, GerritServerConfig.class));
       sysInjector = createSysInjector();
@@ -298,15 +302,13 @@
     modules.add(new PluginRestApiModule());
     modules.add(new RestCacheAdminModule());
     modules.add(new GpgModule(config));
-    AbstractModule changeIndexModule;
-    switch (IndexModule.getIndexType(cfgInjector)) {
+    switch (indexType) {
       case LUCENE:
-        changeIndexModule = new LuceneIndexModule();
+        modules.add(new LuceneIndexModule());
         break;
       default:
-        throw new IllegalStateException("unsupported index.type");
+        throw new IllegalStateException("unsupported index.type = " + indexType);
     }
-    modules.add(changeIndexModule);
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
@@ -325,12 +327,19 @@
     return cfgInjector.createChildInjector(modules);
   }
 
+  private void initIndexType() {
+    indexType = IndexModule.getIndexType(cfgInjector);
+  }
+
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<>();
     modules.add(sysInjector.getInstance(SshModule.class));
     modules.add(new SshHostKeyModule());
     modules.add(new DefaultCommandModule(false,
         sysInjector.getInstance(DownloadConfig.class)));
+    if (indexType == IndexType.LUCENE) {
+      modules.add(new IndexCommandsModule());
+    }
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index ef64f3b..8bc9bb2 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -16,7 +16,7 @@
 log4j.appender.stderr=org.apache.log4j.ConsoleAppender
 log4j.appender.stderr.target=System.err
 log4j.appender.stderr.layout=org.apache.log4j.PatternLayout
-log4j.appender.stderr.layout.ConversionPattern=[%d] %-5p %c %x: %m%n
+log4j.appender.stderr.layout.ConversionPattern=[%d] [%t] %-5p %c %x: %m%n
 
 # Silence non-critical messages from MINA SSHD.
 #
diff --git a/lib/BUCK b/lib/BUCK
index 1dbac0a..73983da 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -26,9 +26,9 @@
 
 maven_jar(
   name = 'gwtorm_client',
-  id = 'com.google.gerrit:gwtorm:1.14-16-gc4e356a',
-  bin_sha1 = '01225468065812bbe5f27972df6dafa9d796d833',
-  src_sha1 = '3622460ed58684cb33f786e3748637c8eea324f9',
+  id = 'com.google.gerrit:gwtorm:1.14-20-gec13fdc',
+  bin_sha1 = '60c2f2a5584959343ad1b21c3c79ba0fe825ceac',
+  src_sha1 = '4c562a3aafd1c3828217ee178568ed3d34ec86eb',
   license = 'Apache2.0',
   repository = GERRIT,
 )
@@ -58,8 +58,8 @@
 
 maven_jar(
   name = 'guava',
-  id = 'com.google.guava:guava:19.0-rc1',
-  sha1 = '0364538ac107b8943a1f4d68ac50f1b0421bb983',
+  id = 'com.google.guava:guava:19.0-rc2',
+  sha1 = '93e17f60bc524c2610b41c494bb829c11ca89436',
   license = 'Apache2.0',
 )
 
@@ -152,9 +152,17 @@
 )
 
 maven_jar(
+  name = 'derby',
+  id = 'org.apache.derby:derby:10.11.1.1',
+  sha1 = 'df4b50061e8e4c348ce243b921f53ee63ba9bbe1',
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
+maven_jar(
   name = 'h2',
-  id = 'com.h2database:h2:1.3.174',
-  sha1 = '2fb55391f525bc3ef9f320a379d19350af96a554',
+  id = 'com.h2database:h2:1.3.176',
+  sha1 = 'fd369423346b2f1525c413e33f8cf95b09c92cbd',
   license = 'h2',
 )
 
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 081cdd8..aa29d35 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.documentation.Constants;
 
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
@@ -108,7 +110,7 @@
 
         String title;
         try (BufferedReader titleReader = new BufferedReader(
-            new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
+            new InputStreamReader(new FileInputStream(file), UTF_8))) {
           title = titleReader.readLine();
           if (title != null && title.startsWith("[[")) {
             // Generally the first line of the txt is the title. In a few cases the
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK
index ff6e6c5..0ce5817 100644
--- a/lib/bouncycastle/BUCK
+++ b/lib/bouncycastle/BUCK
@@ -9,7 +9,6 @@
   id = 'org.bouncycastle:bcprov-jdk15on:' + VERSION,
   sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
-  exclude = ['META-INF/BCKEY.*'],
 )
 
 maven_jar(
@@ -17,7 +16,6 @@
   id = 'org.bouncycastle:bcpg-jdk15on:' + VERSION,
   sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
-  exclude = ['META-INF/BCKEY.*'],
   deps = [':bcprov'],
 )
 
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index a7244f2..9756f5d 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -3,8 +3,8 @@
 include_defs('//lib/codemirror/closure.defs')
 
 REPO = MAVEN_CENTRAL
-VERSION = '5.5'
-SHA1 = 'd9cee6fe3de8e02372b1ac1e9a627224a4f649a7'
+VERSION = '5.7'
+SHA1 = '839a48a6c8d6b36193832a822911198ccba96bfe'
 
 if REPO == MAVEN_CENTRAL:
   URL = REPO + 'org/webjars/codemirror/%s/codemirror-%s.jar' % (VERSION, VERSION)
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index 8259252..abb6d92 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -9,10 +9,13 @@
   'lib/codemirror.js',
   'mode/meta.js',
   'keymap/vim.js',
+  'keymap/emacs.js',
 ]
 
 CM_ADDONS = [
   'dialog/dialog.js',
+  'edit/closebrackets.js',
+  'edit/matchbrackets.js',
   'edit/trailingspace.js',
   'scroll/annotatescrollbar.js',
   'scroll/simplescrollbars.js',
@@ -78,6 +81,7 @@
   'tcl',
   'velocity',
   'verilog',
+  'vhdl',
   'xml',
   'yaml',
 ]
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK
index 3e2f411..6876dfe 100644
--- a/lib/gwt/BUCK
+++ b/lib/gwt/BUCK
@@ -15,15 +15,6 @@
   id = 'com.google.gwt:gwt-dev:' + VERSION,
   sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982',
   license = 'Apache2.0',
-  exported_deps = [
-    ':javax-validation',
-    ':javax-validation_src',
-    '//lib/ow2:ow2-asm',
-    '//lib/ow2:ow2-asm-analysis',
-    '//lib/ow2:ow2-asm-commons',
-    '//lib/ow2:ow2-asm-tree',
-    '//lib/ow2:ow2-asm-util',
-  ],
   attach_source = False,
   exclude = ['org/eclipse/jetty/*'],
 )
@@ -34,7 +25,7 @@
   bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
   src_sha1 = '7a561191db2203550fbfa40d534d4997624cd369',
   license = 'Apache2.0',
-  visibility = [],
+  visibility = ['PUBLIC'],
 )
 
 maven_jar(
@@ -54,5 +45,5 @@
   id = 'org.javassist:javassist:3.18.1-GA',
   sha1 = 'd9a09f7732226af26bf99f19e2cffe0ae219db5b',
   license = 'Apache2.0',
-  visibility = [],
+  visibility = ['PUBLIC'],
 )
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 5e4a82f..c5107d5 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,6 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '5.2.1'
+VERSION = '5.3.0'
 
 # core and backward-codecs both provide
 # META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
@@ -16,7 +16,7 @@
 maven_jar(
   name = 'core_jar',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'a175590aa8b04e079eb1a136fd159f9163482ba4',
+  sha1 = '9e12bb7c39e964a544e3a23b9c8ffa9599d38f10',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -28,7 +28,7 @@
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = '33b7cc17d5a7c939af6fe3f67563f4709926d7f5',
+  sha1 = '1502beac94cf437baff848ffbbb8f76172befa6b',
   license = 'Apache2.0',
   deps = [':core-and-backward-codecs'],
   exclude = [
@@ -40,7 +40,7 @@
 maven_jar(
   name = 'backward-codecs_jar',
   id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
-  sha1 = '603d1f06b133449272799d698e5118db65e523ba',
+  sha1 = 'f654901e55fe56bdbe4be202767296929c2f8d9e',
   license = 'Apache2.0',
   deps = [':core_jar'],
   exclude = [
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'misc',
   id = 'org.apache.lucene:lucene-misc:' + VERSION,
-  sha1 = 'be0a4f0ac06f0a2fa3689b4bf6cd1fe6847f9969',
+  sha1 = 'd03ce6d1bb8ab3926b3acc717418c474a49ade69',
   license = 'Apache2.0',
   deps = [':core-and-backward-codecs'],
   exclude = [
@@ -65,7 +65,7 @@
 maven_jar(
   name = 'queryparser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = '73be0a2d4ab3e6b574be1938bfb27f7f730f0ad9',
+  sha1 = '2c5e08580316c90b56a52e3cb686e1cf69db3f9e',
   license = 'Apache2.0',
   deps = [':core-and-backward-codecs'],
   exclude = [
diff --git a/lib/maven.defs b/lib/maven.defs
index dd8097e..a16fda1 100644
--- a/lib/maven.defs
+++ b/lib/maven.defs
@@ -123,7 +123,7 @@
   else:
     srcjar = None
     genrule(
-      name = '%s__download_src' % name,
+      name = '%s_src' % name,
       cmd = ':>$OUT',
       out = '__%s__no_src' % name,
     )
diff --git a/lib/prolog/prolog.defs b/lib/prolog/prolog.defs
index 677a9e2..e74c21d 100644
--- a/lib/prolog/prolog.defs
+++ b/lib/prolog/prolog.defs
@@ -33,7 +33,6 @@
   genrule(
     name = name + '__ln',
     cmd = 'ln -s $(location :%s__lib) $OUT' % name,
-    deps = [':%s__lib' % name],
     out = name + '.jar',
   )
   prebuilt_jar(
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index ec6ed89..226f4d4 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e
+Subproject commit 226f4d41673257bc5b6f95deae49a49aaabde750
diff --git a/plugins/download-commands b/plugins/download-commands
index 6d4e0a4..86eb557 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 6d4e0a45ad4d7faebc692e5f10e418cbfcf858cb
+Subproject commit 86eb55733599fee4a996be70bacc842586cd9fae
diff --git a/plugins/replication b/plugins/replication
index cc91e0c..32e84b1 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit cc91e0c2987a4606e19b10e320b14f6a0c911c06
+Subproject commit 32e84b1b6131230caf880b0c181f98551edc44c6
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 6fb0101..f6df712 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 6fb010107a7dfdd6baff2b54e65fb74c933d6654
+Subproject commit f6df7121d2704e73c2a315a660e5cc4e12ab1ab9
diff --git a/tools/BUCK b/tools/BUCK
index 0bdff3c..e311ff8 100644
--- a/tools/BUCK
+++ b/tools/BUCK
@@ -42,7 +42,7 @@
   return environ.get('PATH')
 
 genrule(
-  name = 'buck.properties',
+  name = 'buck',
   cmd = 'echo buck=`which buck`>$OUT;' +
     ("echo PATH=\''%s'\' >>$OUT;" % shquote(os_path())),
   deps = [],
diff --git a/tools/build.defs b/tools/build.defs
index da07c1e..893abba 100644
--- a/tools/build.defs
+++ b/tools/build.defs
@@ -42,15 +42,13 @@
     ):
   cmd = ['$(exe //tools:pack_war)', '-o', '$OUT', '--tmp', '$TMP']
   for l in libs:
-    cmd.extend(['--lib', l])
+    cmd.extend(['--lib', '$(classpath %s)' % l])
   for l in pgmlibs:
-    cmd.extend(['--pgmlib', l])
+    cmd.extend(['--pgmlib', '$(classpath %s)' % l])
 
-  dep = []
   if docs:
     cmd.append('$(location %s)' % DOCS_HTML)
-    dep.append(DOCS_LIB)
-    cmd.extend(['--lib', DOCS_LIB])
+    cmd.extend(['--lib', '$(classpath %s)' % DOCS_LIB])
   if context:
     for t in context:
       cmd.append('$(location %s)' % t)
@@ -58,7 +56,6 @@
   genrule(
     name = name,
     cmd = ' '.join(cmd),
-    deps = libs + pgmlibs + dep,
     out = name + '.war',
     visibility = visibility,
   )
diff --git a/tools/default.defs b/tools/default.defs
index 543bf98..7a6e982 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -132,7 +132,8 @@
     manifest_file = None,
     manifest_entries = [],
     type = 'plugin',
-    visibility = ['PUBLIC']):
+    visibility = ['PUBLIC'],
+    target_suffix = ''):
   tb = traceback.extract_stack()
   calling_BUCK_file = tb[-2][0]
   calling_BUCK_dir = os.path.abspath(os.path.dirname(calling_BUCK_file))
@@ -193,7 +194,7 @@
     gwt_binary(
       name = name + '__gwt_application',
       modules = [gwt_module],
-      deps = GWT_PLUGIN_DEPS + ['//lib/gwt:dev'],
+      deps = GWT_PLUGIN_DEPS + GWT_TRANSITIVE_DEPS + ['//lib/gwt:dev'],
       module_deps = [':%s__gwt_module' % name],
       local_workers = cpu_count(),
       strict = True,
@@ -202,7 +203,7 @@
     )
 
   java_binary(
-    name = name,
+    name = name + target_suffix,
     manifest_file = ':%s__manifest' % name,
     merge_manifests = False,
     deps = [
diff --git a/tools/download_all.py b/tools/download_all.py
index 3b21882..58316ca 100755
--- a/tools/download_all.py
+++ b/tools/download_all.py
@@ -32,7 +32,7 @@
   if m:
     n = m.group(1)
     if args.src and n.endswith('__download_bin'):
-      n = n[:-4] + '_src'
+      n = n[:-13] + 'src'
     targets.add(n)
 r = p.wait()
 if r != 0:
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index 57f3afb..1e13515 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -21,6 +21,8 @@
     '//lib/bouncycastle:bcprov',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcpkix',
+    '//lib/gwt:javax-validation',
+    '//lib/gwt:javax-validation_src',
     '//lib/jetty:servlets',
     '//lib/prolog:compiler_lib',
     '//Documentation:index_lib',
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index f3300fa..67ab138 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -169,7 +169,7 @@
     for j in sorted(libs):
       s = None
       if j.endswith('.jar'):
-        s = j[:-4] + '-src.jar'
+        s = j[:-4] + '_src.jar'
         if not path.exists(s):
           s = None
       if args.plugins:
@@ -224,8 +224,8 @@
   gen_factorypath()
 
   try:
-    targets = ['//tools:buck.properties'] + MAIN + GWT
-    check_call(['buck', 'build'] + targets)
+    targets = ['//tools:buck'] + MAIN + GWT
+    check_call(['buck', 'build', '--deep'] + targets)
   except CalledProcessError as err:
     exit(1)
 except KeyboardInterrupt:
diff --git a/tools/gwt-constants.defs b/tools/gwt-constants.defs
index 2584f2d..8bafddb 100644
--- a/tools/gwt-constants.defs
+++ b/tools/gwt-constants.defs
@@ -12,3 +12,13 @@
   '//gerrit-plugin-gwtui:gwtui-api-lib',
   '//lib/gwt:user',
 ]
+
+GWT_TRANSITIVE_DEPS = [
+  '//lib/gwt:javax-validation',
+  '//lib/gwt:javax-validation_src',
+  '//lib/ow2:ow2-asm',
+  '//lib/ow2:ow2-asm-analysis',
+  '//lib/ow2:ow2-asm-commons',
+  '//lib/ow2:ow2-asm-tree',
+  '//lib/ow2:ow2-asm-util',
+]
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
index fdc01a8..98a7ade 100644
--- a/tools/maven/BUCK
+++ b/tools/maven/BUCK
@@ -10,16 +10,19 @@
   url = URL,
   version = GERRIT_VERSION,
   jar = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework',
     'gerrit-extension-api': '//gerrit-extension-api:extension-api',
     'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api',
     'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api',
   },
   src = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework-src',
     'gerrit-extension-api': '//gerrit-extension-api:extension-api-src',
     'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api-src',
     'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api-src',
   },
   doc = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework-javadoc',
     'gerrit-extension-api': '//gerrit-extension-api:extension-api-javadoc',
     'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api-javadoc',
     'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api-javadoc',
diff --git a/tools/pack_war.py b/tools/pack_war.py
index cfa7e36..8525a56 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -16,7 +16,7 @@
 from __future__ import print_function
 from optparse import OptionParser
 from os import chdir, makedirs, path, symlink
-from subprocess import check_call, check_output
+from subprocess import check_call
 import sys
 
 opts = OptionParser()
@@ -30,17 +30,14 @@
 root = war[:war.index('buck-out')]
 jars = set()
 
+def prune(l):
+ return [j[j.find('buck-out'):] for e in l for j in e.split(':')]
 
 def link_jars(libs, directory):
   makedirs(directory)
   while not path.isfile('.buckconfig'):
     chdir('..')
-  try:
-    cp = check_output(['buck', 'audit', 'classpath'] + libs)
-  except Exception as e:
-    print('call to buck audit failed: %s' % e, file=sys.stderr)
-    exit(1)
-  for j in cp.strip().splitlines():
+  for j in libs:
     if j not in jars:
       jars.add(j)
       n = path.basename(j)
@@ -49,9 +46,9 @@
       symlink(path.join(root, j), path.join(directory, n))
 
 if args.lib:
-  link_jars(args.lib, path.join(war, 'WEB-INF', 'lib'))
+  link_jars(prune(args.lib), path.join(war, 'WEB-INF', 'lib'))
 if args.pgmlib:
-  link_jars(args.pgmlib, path.join(war, 'WEB-INF', 'pgm-lib'))
+  link_jars(prune(args.pgmlib), path.join(war, 'WEB-INF', 'pgm-lib'))
 try:
   for s in ctx:
     check_call(['unzip', '-q', '-d', war, s])
diff --git a/tools/version.py b/tools/version.py
index e2d9ead..9f03a59 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -45,10 +45,10 @@
 
 src_pattern = re.compile(r'^(\s*<version>)([-.\w]+)(</version>\s*)$',
                          re.MULTILINE)
-for project in ['gerrit-extension-api', 'gerrit-plugin-api',
-                'gerrit-plugin-archetype', 'gerrit-plugin-gwt-archetype',
-                'gerrit-plugin-gwtui', 'gerrit-plugin-js-archetype',
-                'gerrit-war']:
+for project in ['gerrit-acceptance-framework', 'gerrit-extension-api',
+                'gerrit-plugin-api', 'gerrit-plugin-archetype',
+                'gerrit-plugin-gwt-archetype', 'gerrit-plugin-gwtui',
+                'gerrit-plugin-js-archetype', 'gerrit-war']:
   pom = os.path.join(project, 'pom.xml')
   replace_in_file(pom, src_pattern)