Merge branch 'stable-2.7' into stable-2.8

* stable-2.7:
  Use rev-parse to find gitdir when generating commit-msg hook hint

Conflicts:
	gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java

Change-Id: Ib6b05123e34b9a87a2e9bc735f05a443bdc13756
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..1bc29ac
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,16 @@
+[alias]
+  api = //:api
+  api_deploy = //tools/maven:deploy
+  api_install = //tools/maven:install
+  docs = //Documentation:html
+  gerrit = //:gerrit
+  release = //:release
+
+[buildfile]
+  includes = //tools/default.defs
+
+[java]
+  src_roots = java, resources
+
+[project]
+  ignore = .git
diff --git a/.buckversion b/.buckversion
new file mode 100644
index 0000000..e39bcf8
--- /dev/null
+++ b/.buckversion
@@ -0,0 +1 @@
+274acb17e9b6dc9ee60bc1371c47a7f49640c24c
diff --git a/.gitignore b/.gitignore
index c87c26f..1b4c29c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,17 @@
 /.classpath
 /.project
-/.settings
-/.settings/org.eclipse.jdt.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/.settings/org.eclipse.ltk.core.refactoring.prefs
 /test_site
 /.idea
 /gerrit-parent.iml
 *.sublime-*
+/gerrit-package-plugins
+/.buckconfig.local
+/.buckd
+/buck-cache
+/buck-out
+/local.properties
+*.pyc
+/gwt-unitCache
diff --git a/.gitmodules b/.gitmodules
index 0f7fdab..6476c4c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,11 +1,20 @@
+[submodule "plugins/commit-message-length-validator"]
+	path = plugins/commit-message-length-validator
+	url = ../plugins/commit-message-length-validator
+
+[submodule "plugins/cookbook-plugin"]
+	path = plugins/cookbook-plugin
+	url = ../plugins/cookbook-plugin
+
+[submodule "plugins/download-commands"]
+	path = plugins/download-commands
+	url = ../plugins/download-commands
+
 [submodule "plugins/replication"]
 	path = plugins/replication
-	url = https://gerrit.googlesource.com/plugins/replication
+	url = ../plugins/replication
 
 [submodule "plugins/reviewnotes"]
 	path = plugins/reviewnotes
-	url = https://gerrit.googlesource.com/plugins/reviewnotes
+	url = ../plugins/reviewnotes
 
-[submodule "plugins/commit-message-length-validator"]
-	path = plugins/commit-message-length-validator
-	url = https://gerrit.googlesource.com/plugins/commit-message-length-validator
diff --git a/.pydevproject b/.pydevproject
new file mode 100644
index 0000000..be43141
--- /dev/null
+++ b/.pydevproject
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6.5</pydev_property>
+</pydev_project>
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..29abf99
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/.settings/org.eclipse.core.runtime.prefs b/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..dbc83d5
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,346 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
+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_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
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+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_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_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.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
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+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_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
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+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.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.format_guardian_clause_on_one_line=false
+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
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+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_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_parameter=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_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
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+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_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
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+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_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_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
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+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_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
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+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_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
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+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_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
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+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_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
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+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_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..d4218a5
--- /dev/null
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..616a0fe
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,78 @@
+include_defs('//tools/build.defs')
+
+gerrit_war(name = 'gerrit')
+gerrit_war(name = 'chrome',   ui = 'ui_chrome')
+gerrit_war(name = 'firefox',  ui = 'ui_firefox')
+gerrit_war(name = 'withdocs', context = DOCS)
+gerrit_war(name = 'release',  context = DOCS + ['//plugins:core.zip'])
+
+API_DEPS = [
+  ':extension-api',
+  ':extension-api-src',
+  ':plugin-api',
+  ':plugin-api-src',
+]
+
+genrule(
+  name = 'api',
+  cmd = ';'.join(
+    ['cd $TMP'] +
+    ['ln -s $(location %s) .' % n for n in API_DEPS] +
+    ['zip -q0 $OUT *']),
+  deps = API_DEPS,
+  out = 'api.zip',
+)
+
+java_binary(
+  name = 'extension-api',
+  deps = [':extension-lib'],
+  visibility = ['//tools/maven:'],
+)
+
+java_library(
+  name = 'extension-lib',
+  deps = [
+    '//gerrit-extension-api:api',
+    '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
+    '//lib:servlet-api-3_0',
+  ],
+  export_deps = True,
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'extension-api-src',
+  cmd = 'ln -s $(location //gerrit-extension-api:api-src) $OUT',
+  deps = ['//gerrit-extension-api:api-src'],
+  out = 'extension-api-src.jar',
+  visibility = ['//tools/maven:'],
+)
+
+PLUGIN_API = [
+  '//gerrit-server:server',
+  '//gerrit-pgm:init-api',
+  '//gerrit-sshd:sshd',
+  '//gerrit-httpd:httpd',
+]
+
+java_binary(
+  name = 'plugin-api',
+  deps = [':plugin-lib'],
+  visibility = ['//tools/maven:'],
+)
+
+java_library(
+  name = 'plugin-lib',
+  deps = PLUGIN_API + ['//lib:servlet-api-3_0'],
+  export_deps = True,
+  visibility = ['PUBLIC'],
+)
+
+java_binary(
+  name = 'plugin-api-src',
+  deps = [
+    '//gerrit-extension-api:api-src',
+  ] + [d + '-src' for d in PLUGIN_API],
+  visibility = ['//tools/maven:'],
+)
diff --git a/Documentation/.gitignore b/Documentation/.gitignore
deleted file mode 100644
index 8a3da24..0000000
--- a/Documentation/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*.html
-/.published
diff --git a/Documentation/BUCK b/Documentation/BUCK
new file mode 100644
index 0000000..71d8664
--- /dev/null
+++ b/Documentation/BUCK
@@ -0,0 +1,81 @@
+include_defs('//Documentation/asciidoc.defs')
+include_defs('//Documentation/config.defs')
+include_defs('//tools/git.defs')
+
+DOC_DIR = 'Documentation'
+INDEX_DIR = DOC_DIR + '/.index'
+MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
+
+genrule(
+  name = 'html',
+  cmd = 'cd $TMP;' +
+    'mkdir -p %s/images;' % DOC_DIR +
+    'unzip -q $SRCDIR/index.zip -d %s/;' % INDEX_DIR +
+    'unzip -q $SRCDIR/only_html.zip -d %s/;' % DOC_DIR +
+    'for s in $SRCS;do ln -s $s %s;done;' % DOC_DIR +
+    'mv %s/*.{jpg,png} %s/images;' % (DOC_DIR, DOC_DIR) +
+    'rm %s/only_html.zip;' % DOC_DIR +
+    'rm %s/index.zip;' % DOC_DIR +
+    'rm %s/licenses.txt;' % DOC_DIR +
+    'cp $SRCDIR/licenses.txt LICENSES.txt;' +
+    'zip -qr $OUT *',
+  srcs = glob([
+      'images/*.jpg',
+      'images/*.png',
+    ]) + [
+      'doc.css',
+      genfile('licenses.txt'),
+      genfile('only_html.zip'),
+      genfile('index.zip'),
+    ],
+  deps = [
+    ':generate_html',
+    ':index',
+    ':licenses.txt',
+  ],
+  out = 'html.zip',
+  visibility = ['PUBLIC'],
+)
+
+genasciidoc(
+  name = 'generate_html',
+  srcs = SRCS + [genfile('licenses.txt')],
+  deps = [':licenses.txt'],
+  attributes = documentation_attributes(git_describe()),
+  backend = 'html5',
+  out = 'only_html.zip',
+)
+
+genrule(
+  name = 'licenses.txt',
+  cmd = '$(exe :gen_licenses) >$OUT',
+  deps = [':gen_licenses'] + MAIN,
+  out = 'licenses.txt',
+)
+
+python_binary(
+  name = 'gen_licenses',
+  main = 'gen_licenses.py',
+)
+
+python_binary(
+  name = 'replace_macros',
+  main = 'replace_macros.py',
+)
+
+genrule(
+  name = 'index',
+  cmd = '$(exe //lib/asciidoctor:doc_indexer) ' +
+      '-z $OUT ' +
+      '--prefix "%s/" ' % DOC_DIR +
+      '--in-ext ".txt" ' +
+      '--out-ext ".html" ' +
+      '$SRCS',
+  srcs = SRCS + [genfile('licenses.txt')],
+  deps = [
+    ':licenses.txt',
+    '//lib/asciidoctor:doc_indexer',
+  ],
+  out = 'index.zip',
+)
diff --git a/Documentation/GEN-DOC-VERSION b/Documentation/GEN-DOC-VERSION
deleted file mode 100755
index 973bfa8..0000000
--- a/Documentation/GEN-DOC-VERSION
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/sh
-
-V=$(git describe HEAD)
-
-case "$V" in
-'')
-	echo >&2 "fatal: no annotated tags, cannot determine version"
-	exit 1
-	;;
-
-*-g*)
-	echo >&2 "fatal: snapshot $V, cannot determine version"
-	exit 1
-	;;
-
-v*)
-	echo "$V" | perl -lne 'print $1 if /^v(\d+\.\d+(?:\.\d+)?)/'
-	;;
-esac
diff --git a/Documentation/Makefile b/Documentation/Makefile
deleted file mode 100644
index 59de209..0000000
--- a/Documentation/Makefile
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright (C) 2009 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-ASCIIDOC       ?= asciidoc
-ASCIIDOC_EXTRA ?=
-ASCIIDOC_VER   ?= 8.6.3
-SVN            ?= svn
-PUB_ROOT       ?= https://gerrit-documentation.googlecode.com/svn/Documentation
-
-all: html
-
-clean:
-	rm -f *.html
-	rm -rf $(LOCAL_ROOT)
-
-ASCIIDOC_EXE := $(shell which $(ASCIIDOC))
-ifeq ($(wildcard $(ASCIIDOC_EXE)),)
-  $(error $(ASCIIDOC) must be available)
-else
-  ASCIIDOC_OK := $(shell expr `asciidoc --version | cut -f2 -d' '` \>= $(ASCIIDOC_VER))
-  ifeq ($(ASCIIDOC_OK),0)
-    $(error $(ASCIIDOC) version $(ASCIIDOC_VER) or higher is required)
-  endif
-endif
-
-ifeq ($(origin VERSION), undefined)
-  VERSION := $(shell ./GEN-DOC-VERSION 2>/dev/null)
-endif
-
-DOC_HTML      := $(patsubst %.txt,%.html,$(wildcard *.txt))
-LOCAL_ROOT    := .published
-COMMIT        := $(shell git describe HEAD | sed s/^v//)
-PUB_DIR       := $(PUB_ROOT)/$(VERSION)
-PRIOR          = PRIOR
-
-ifeq ($(VERSION),)
-  REVISION = $(COMMIT)
-else
-  ifeq ($(VERSION),$(COMMIT))
-    REVISION := $(VERSION)
-  else
-    REVISION := $(VERSION) (from v$(COMMIT))
-  endif
-endif
-
-html: $(DOC_HTML)
-
-update: html
-ifeq ($(VERSION),)
-	./GEN-DOC-VERSION
-endif
-	@-rm -rf $(LOCAL_ROOT)
-	@echo "Checking out current $(VERSION)"
-	@if ! $(SVN) checkout $(PUB_DIR) $(LOCAL_ROOT) 2>/dev/null ; then \
-		echo "Copying $(PRIOR) to $(VERSION) ..." && \
-		$(SVN) cp -m "Create $(VERSION) documentation" $(PUB_ROOT)/$(PRIOR) $(PUB_DIR) && \
-		$(SVN) checkout $(PUB_DIR) $(LOCAL_ROOT) ; \
-	fi
-	@rm -f $(LOCAL_ROOT)/*.html
-	@cp *.html $(LOCAL_ROOT)
-	@cd $(LOCAL_ROOT) && \
-	  r=`$(SVN) status | perl -ne 'print if s/^!  *//' ` && \
-	  if [ -n "$$r" ]; then $(SVN) rm $$r; fi && \
-	  a=`$(SVN) status | perl -ne 'print if s/^\?  *//' ` && \
-	  if [ -n "$$a" ]; then \
-	    $(SVN) add $$a && \
-	    $(SVN) propset svn:mime-type text/html $$a ; \
-	    fi && \
-	  echo "Committing $(VERSION) at v$(COMMIT)" && \
-	  $(SVN) commit -m "Updated $(VERSION) documentation to v$(COMMIT)"
-	@-rm -rf $(LOCAL_ROOT)
-
-$(DOC_HTML): %.html : %.txt
-	@echo "FORMAT $@"
-	@rm -f $@+ $@
-	@$(ASCIIDOC) -a toc \
-		-a data-uri \
-		-a 'revision=$(REVISION)' \
-		-a 'newline=\n' \
-		-b xhtml11 \
-		-f asciidoc.conf \
-		$(ASCIIDOC_EXTRA) \
-		-o $@+ $<
-	@mv $@+ $@
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index ab32d78..189316c 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -348,7 +348,7 @@
 
 This is where the Gerrit configuration of each project is residing.  This
 branch contains several files of importance: +project.config+, +groups+ and
-+rules.pl+.  Torgether they control access and behaviour during the change
++rules.pl+.  Torgether they control access and behavior during the change
 review process.
 
 
@@ -374,7 +374,6 @@
 These are references with added functionality to them compared to a regular
 git push operation.
 
-
 refs/for/<branch ref>
 ^^^^^^^^^^^^^^^^^^^^^
 
@@ -410,10 +409,6 @@
 Gerrit has several permission categories that can be granted to groups
 within projects, enabling functionality for that group's members.
 
-With the release of the Gerrit 2.2.x series, the web GUI for ACL
-configuration was rewritten from scratch.  Use this
-<<conversion_table,table>> to better understand the access rights
-conversions from the Gerrit 2.1.x to the Gerrit 2.2.x series.
 
 
 [[category_abandon]]
@@ -424,8 +419,9 @@
 to projects in Gerrit. It can give permission to abandon a specific
 change to a given ref.
 
-This also grants the permission to restore a change if the change
-can be uploaded.
+This also grants the permission to restore a change if the user also
+has link:#category_push[push permission] on the change's destination
+ref.
 
 
 [[category_create]]
@@ -604,7 +600,7 @@
 
 [[category_push_merge]]
 Push Merge Commits
-~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~
 
 The `Push Merge Commit` access right permits the user to upload merge
 commits.  It's an add-on to the <<category_push,Push>> access right, and
@@ -752,7 +748,8 @@
 
 For every configured label `My-Name` in the project, there is a
 corresponding permission `label-My-Name` with a range corresponding to
-the defined values.
+the defined values. There is also a corresponding `labelAs-My-Name`
+permission that enables editing another user's label.
 
 Gerrit comes pre-configured with a default 'Code-Review' label that can
 be granted to groups within projects, enabling functionality for that
@@ -825,6 +822,10 @@
 can always edit the topic name (even without having the `Edit Topic Name`
 access right assigned).
 
+Whether the topic can be edited on closed changes can be controlled
+by the 'Force Edit' flag. If this flag is not set the topic can only be
+edited on open changes.
+
 
 Examples of typical roles in a project
 --------------------------------------
@@ -875,7 +876,7 @@
 * xref:category_push_merge[`Push merge commit`] to 'refs/for/refs/heads/*'
 * xref:category_forge_author[`Forge Author Identity`] to 'refs/heads/*'
 * link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-2' to '+2' for 'refs/heads/*'
-* link:config-labels.html#label_Verified[`Label: Verify`] with range '-1' to '+1' for 'refs/heads/*'
+* link:config-labels.html#label_Verified[`Label: Verified`] with range '-1' to '+1' for 'refs/heads/*'
 * xref:category_submit[`Submit`]
 
 If the project is small or the developers are seasoned it might make
@@ -903,15 +904,15 @@
 * An unstable build (tests fails)
 * A failed build
 
-Usually the range chosen for this verdict is the verify label.  Depending on
+Usually the range chosen for this verdict is the `Verified` label.  Depending on
 the size of your project and discipline of involved developers you might want
-to limit access right to the +1 `Verify` label to the CI system only.  That
+to limit access right to the +1 `Verified` label to the CI system only.  That
 way it's guaranteed that submitted commits always get built and pass tests
 successfully.
 
 If the build doesn't complete successfully the CI system can set the
-`Verify` label to -1.  However that means that a failed build will block
-submit of the change even if someone else sets `Verify` +1.  Depending on the
+`Verified` label to -1.  However that means that a failed build will block
+submit of the change even if someone else sets `Verified` +1.  Depending on the
 project and how much the CI system can be trusted for accurate results, a
 blocking label might not be feasible.  A recommended alternative is to set the
 label `Code-review` to -1 instead, as it isn't a blocking label but still
@@ -928,7 +929,7 @@
 
 * xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
 * link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-1' to '0' for 'refs/heads/*'
-* link:config-labels.html#label_Verified[`Label: Verify`] with range '0' to '+1' for 'refs/heads/*'
+* link:config-labels.html#label_Verified[`Label: Verified`] with range '0' to '+1' for 'refs/heads/*'
 
 Optional access rights to grant:
 
@@ -1118,43 +1119,6 @@
     label-Release-Process = -1..+1 group Release Engineers
 ====
 
-
-[[conversion_table]]
-Conversion table from 2.1.x series to 2.2.x series
---------------------------------------------------
-
-[options="header"]
-|=================================================================================
-|Gerrit 2.1.x                 |Gerrit 2.2.x
-|Code review                  |link:config-labels.html#label_Code-Review[Label: Code-Review]
-|Verify                       |link:config-labels.html#label_Verified[Label: Verify]
-|Forge Identity +1            |Forge <<category_forge_author,author>> identity
-|Forge Identity +2            |Forge <<category_forge_committer,committer>> & <<category_forge_author,author>> identity
-|Forge Identity +3            |Forge <<category_forge_server,server>> & <<category_forge_committer,committer>> & <<category_forge_author,author>> identity
-|Owner                        |<<category_owner,Owner>>
-|Push branch +1               |<<category_push_direct,Push>>
-|Push branch +2               |<<category_create,Create reference>> & <<category_push_direct,Push>>
-|Push branch +3               |<<category_push_direct,Push>> (with force) & <<category_create,Create reference>>
-|Push tag +1 & Push Branch +2 |No support to limit to push signed tag
-|Push tag +2 & Push Branch +2 |<<category_push_annotated,Push annotated tag>>
-|Push Branch +2 (refs/tags/*) |<<category_create,Create reference>> (refs/tags/...)
-|Push Branch +3 (refs/tags/*) |<<category_push_direct,Push>> (with force on refs/tags/...)
-|Read +1                      |<<category_read,Read>>
-|Read +2                      |<<category_read,Read>> & <<category_push_review,Push>> (refs/for/refs/...)
-|Read +3                      |<<category_read,Read>> & <<category_push_review,Push>> (refs/for/refs/...) & <<category_push_merge,Push Merge Commit>>
-|Submit                       |<<category_submit,Submit>>
-|=================================================================================
-
-
-[NOTE]
-In Gerrit 2.2.x, the way to set permissions for upload has changed entirely.
-To upload a change for review is no longer a separate permission type,
-instead you grant ordinary push permissions to the actual
-receiving reference. In practice this means that you set push permissions
-on `refs/for/refs/heads/<branch>` rather than permissions to upload changes
-on `refs/heads/<branch>`.
-
-
 [[global_capabilities]]
 Global Capabilities
 -------------------
@@ -1176,6 +1140,13 @@
 Below you find a list of capabilities available:
 
 
+[[capability_accessDatabase]]
+Access Database
+~~~~~~~~~~~~~~~
+
+Allow users to access the database using the `gsql` command.
+
+
 [[capability_administrateServer]]
 Administrate Server
 ~~~~~~~~~~~~~~~~~~~
@@ -1238,6 +1209,14 @@
 you need the <<capability_viewCaches,view caches capability>>.
 
 
+[[capability_generateHttpPassword]]
+Generate HTTP Password
+~~~~~~~~~~~~~~~~~~~~~~
+
+Allow the user to generate HTTP passwords for other users.  Typically this would
+be assigned to a non-interactive users group.
+
+
 [[capability_kill]]
 Kill Task
 ~~~~~~~~~
@@ -1283,8 +1262,8 @@
 
 Allow site administrators to configure the query limit for users to
 be above the default hard-coded value of 500.  Administrators can add
-a global block to `All-Projects` with group(s) that
-should have different limits:
+a global block to `All-Projects` with group(s) that should have different
+limits.
 
 When applying a query limit to a user the largest value granted by
 any of their groups is used.
@@ -1293,11 +1272,25 @@
 command, but also to the web UI results pagination size.
 
 
-[[capability_accessDatabase]]
-Access Database
-~~~~~~~~~~~~~~~
+[[capability_runAs]]
+Run As
+~~~~~~
 
-Allow users to access the database using the `gsql` command.
+Allow users to impersonate any other user with the `X-Gerrit-RunAs`
+HTTP header on REST API calls, or the link:cmd-suexec.html[suexec]
+SSH command.
+
+When impersonating an administrator the Administrate Server capability
+is not honored.  This security feature tries to prevent a role with
+Run As capability from modifying the access controls in All-Projects,
+however modification may still be possible if the impersonated user
+has permission to push or submit changes on `refs/meta/config`.  Run
+As also blocks using most capabilities including Create User, Run
+Garbage Collection, etc., unless the capability is also explicitly
+granted to a group the administrator is a member of.
+
+Administrators do not automatically inherit this capability; it must
+be explicitly granted.
 
 
 [[capability_runGC]]
@@ -1308,14 +1301,6 @@
 all projects.
 
 
-[[capability_startReplication]]
-Start Replication
-~~~~~~~~~~~~~~~~~
-
-Allow access to execute `replication start` command, if the
-replication plugin is installed on the server.
-
-
 [[capability_streamEvents]]
 Stream Events
 ~~~~~~~~~~~~~
diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf
deleted file mode 100644
index 2fe6213..0000000
--- a/Documentation/asciidoc.conf
+++ /dev/null
@@ -1,29 +0,0 @@
-[attributes]
-asterisk=&#42;
-plus=&#43;
-caret=&#94;
-startsb=&#91;
-endsb=&#93;
-tilde=&#126;
-
-[specialsections]
-GERRIT=gerrituplink
-
-[gerrituplink]
-<hr style="
-  height: 2px;
-  color: silver;
-  margin-top: 1.2em;
-  margin-bottom: 0.5em;
-">
-
-[macros]
-(?u)^(?P<name>get)::(?P<target>\S*?)$=#
-
-[get-blockmacro]
-<a id="{target}" onmousedown="javascript:
-  var i =  document.URL.lastIndexOf('/Documentation/');
-  var url = document.URL.substring(0, i) + '{target}';
-  document.getElementById('{target}').href = url;">
-    GET {target} HTTP/1.0
-</a>
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
new file mode 100644
index 0000000..44313d2
--- /dev/null
+++ b/Documentation/asciidoc.defs
@@ -0,0 +1,67 @@
+# 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.
+
+def genasciidoc(
+    name,
+    out,
+    srcs = [],
+    deps = [],
+    attributes = [],
+    backend = None,
+    visibility = []):
+  EXPN = '.expn'
+
+  asciidoc = [
+      'cd $SRCDIR;',
+      '$(exe //lib/asciidoctor:asciidoc)',
+      '-z', '$OUT',
+      '--in-ext', '".txt%s"' % EXPN,
+      '--out-ext', '".html"',
+  ]
+  if backend:
+    asciidoc.extend(['-b', backend])
+  for attribute in attributes:
+    asciidoc.extend(['-a', attribute])
+  asciidoc.append('$SRCS')
+  newsrcs = ["doc.css"]
+  newdeps = deps + ['//lib/asciidoctor:asciidoc']
+
+  for src in srcs:
+    tx = []
+    fn = src
+    if fn.startswith('BUCKGEN:') :
+      fn = src[8:]
+      tx = [':' + fn]
+    ex = fn + EXPN
+
+    genrule(
+      name = ex,
+      cmd = '$(exe :replace_macros) --suffix=' + EXPN +
+            ' -s $SRCDIR/%s' % fn +
+            ' -o $OUT',
+      srcs = [src],
+      deps = tx + [':replace_macros'],
+      out = ex,
+    )
+    newdeps.append(':' + ex)
+    newsrcs.append(genfile(ex))
+
+  genrule(
+    name = name,
+    cmd = ' '.join(asciidoc),
+    srcs = newsrcs,
+    deps = newdeps,
+    out = out,
+    visibility = visibility,
+  )
diff --git a/Documentation/cmd-cherry-pick.txt b/Documentation/cmd-cherry-pick.txt
index d051a9a..15a8524 100644
--- a/Documentation/cmd-cherry-pick.txt
+++ b/Documentation/cmd-cherry-pick.txt
@@ -39,7 +39,7 @@
 ====
   $ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
 
-  $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
+  $ curl -Lo ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
 ====
 
 GERRIT
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 85b9b56..3ecb764 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -3,7 +3,7 @@
 
 NAME
 ----
-gerrit create-account - Create a new batch/role account.
+gerrit create-account - Create a new user account.
 
 SYNOPSIS
 --------
@@ -24,6 +24,10 @@
 used for batch/role access, such as from an automated build system
 or event monitoring over link:cmd-stream-events.html[gerrit stream-events].
 
+Note, however, that in this case the account is not implicitly added
+to the 'Non-Interactive Users' group.  The account must be explicitly
+added to the group with the `--group` option.
+
 If LDAP authentication is being used, the user account is created
 without checking the LDAP directory.  Consequently users can be
 created in Gerrit that do not exist in the underlying LDAP directory.
@@ -67,10 +71,11 @@
 
 EXAMPLES
 --------
-Create a new user account called `watcher`:
+Create a new batch/role access user account called `watcher` in
+the 'Non-Interactive Users' group.
 
 ====
-	$ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-account --ssh-key - watcher
+	$ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-account --group "'Non-Interactive Users'" --ssh-key - watcher
 ====
 
 GERRIT
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index d0e56fd..e3ad834 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -21,6 +21,7 @@
   [--require-change-id | --id]
   [[--branch <REF> | -b <REF>] ...]
   [--empty-commit]
+  [--max-object-size-limit <N>]
   { <NAME> | --name <NAME> }
 
 DESCRIPTION
@@ -108,6 +109,7 @@
 +
 * FAST_FORWARD_ONLY: produces a strictly linear history.
 * MERGE_IF_NECESSARY: create a merge commit when required.
+* REBASE_IF_NECESSARY: rebase the commit when required.
 * MERGE_ALWAYS: always create a merge commit.
 * CHERRY_PICK: always cherry-pick the commit.
 
@@ -144,6 +146,15 @@
 	Creates an initial empty commit for the Git repository of the
 	project that is newly created.
 
+--max-object-size-limit::
+	Define maximum Git object size for this project. Pushes containing an
+	object larger than this limit will be rejected. This can be used to
+	further limit the global
+  link:config-gerrit.html#receive.maxObjectSizeLimit[receive.maxObjectSizeLimit]
+	and cannot be used to increase that globally set limit.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
 
 EXAMPLES
 --------
diff --git a/Documentation/cmd-gsql.txt b/Documentation/cmd-gsql.txt
index 7ddc41f..3c1fd31 100644
--- a/Documentation/cmd-gsql.txt
+++ b/Documentation/cmd-gsql.txt
@@ -9,7 +9,7 @@
 --------
 [verse]
 'ssh' -p <port> <host> 'gerrit gsql'
-  [--format {PRETTY | JSON}]
+  [--format {PRETTY | JSON | JSON_SINGLE}]
   [-c QUERY]
 
 DESCRIPTION
@@ -26,6 +26,8 @@
 	for reading by a human on a sufficiently wide terminal.
 	In JSON mode records are output as JSON objects using the
 	column names as the property names, one object per line.
+	In JSON_SINGLE mode the whole result set is output as a
+	single JSON object.
 
 -c::
 	Execute the single query statement supplied, and then exit.
@@ -38,7 +40,8 @@
 
 SCRIPTING
 ---------
-Intended for interactive use only, unless format is JSON.
+Intended for interactive use only, unless format is JSON, or
+JSON_SINGLE.
 
 EXAMPLES
 --------
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index bd602c1..c0c1e6c 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -1,17 +1,21 @@
 commit-msg Hook
 ===============
 
+
 NAME
 ----
+
+
 commit-msg - Edit commit messages to insert a `Change-Id` tag.
 
 DESCRIPTION
 -----------
 
+
 A Git hook automatically invoked by `git commit`, and most other
 commit creation tools such as `git citool` or `git gui`.  The Gerrit
 Code Review supplied implementation of this hook is a short shell
-script which automatically inserts a globally unique Change-Id tag
+script which automatically inserts a globally unique `Change-Id` tag
 in the footer of a commit message.  When present, Gerrit uses this
 tag to track commits across cherry-picks and rebases.
 
@@ -40,28 +44,33 @@
 ----
 
 The hook implementation is reasonably intelligent at inserting the
-Change-Id line before any Signed-off-by or Acked-by lines placed
+`Change-Id` line before any `Signed-off-by` or `Acked-by` lines placed
 at the end of the commit message by the author, but if no such
 lines are present then it will just insert a blank line, and add
-the Change-Id at the bottom of the message.
+the `Change-Id` at the bottom of the message.
 
-If a Change-Id line is already present in the message footer, the
-script will do nothing, leaving the existing Change-Id unmodified.
+If a `Change-Id` line is already present in the message footer, the
+script will do nothing, leaving the existing `Change-Id` unmodified.
 This permits amending an existing commit, or allows the user to
 insert the Change-Id manually after copying it from an existing
 change viewed on the web.
 
+The `Change-Id` will not be added if `gerrit.createChangeId` is set
+to `false` in the git config.
+
 OBTAINING
 ---------
-To obtain the 'commit-msg' script use scp, wget or curl to download it
-to your local system from your Gerrit server.
+
+
+To obtain the `commit-msg` script use `scp`, `wget` or `curl` to download
+it to your local system from your Gerrit server.
 
 You can use either of the below commands:
 
 ====
   $ scp -p -P 29418 <your username>@<your Gerrit review server>:hooks/commit-msg <local path to your git>/.git/hooks/
 
-  $ curl -o <local path to your git>/.git/hooks/commit-msg <your Gerrit http URL>/tools/hooks/commit-msg
+  $ curl -Lo <local path to your git>/.git/hooks/commit-msg <your Gerrit http URL>/tools/hooks/commit-msg
 ====
 
 A specific example of this might look something like this:
@@ -70,7 +79,7 @@
 ====
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg ~/duhproject/.git/hooks/
 
-  $ curl -o ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+  $ curl -Lo ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 ====
 
 Make sure the hook file is executable:
@@ -82,6 +91,7 @@
 SEE ALSO
 --------
 
+
 * link:user-changeid.html[Change-Id Lines]
 * link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[git-commit(1)]
 * link:http://www.kernel.org/pub/software/scm/git/docs/githooks.html[githooks(5)]
@@ -89,7 +99,8 @@
 IMPLEMENTATION
 --------------
 
-The hook generates unique Change-Id lines by creating a virtual
+
+The hook generates unique `Change-Id` lines by creating a virtual
 commit object within the local Git repository, and obtaining the
 SHA-1 hash from it.  Like any other Git commit, the following
 properties are included in the computation:
@@ -98,12 +109,14 @@
 * SHA-1 of the parent commit
 * Name, email address, timestamp of the author
 * Name, email address, timestamp of the committer
-* Proposed commit message (before Change-Id was inserted)
+* Proposed commit message (before `Change-Id` was inserted)
 
 Because the names of the tree and parent commit, as well as the
 committer timestamp are included in the hash computation, the output
-Change-Id is sufficiently unique.
+`Change-Id` is sufficiently unique.
 
 GERRIT
 ------
+
+
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 780231c..3ba66a8 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -12,8 +12,8 @@
   $ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
 
-  $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
-  $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+  $ curl -Lo ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
+  $ curl -Lo .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 
 For more details on how to determine the correct SSH port number,
 see link:user-upload.html#test_ssh[Testing Your SSH Connection].
@@ -60,6 +60,9 @@
 link:cmd-ls-groups.html[gerrit ls-groups]::
 	List groups visible to the caller.
 
+link:cmd-ls-members.html[gerrit ls-members]::
+	List the membership of a group visible to the caller.
+
 link:cmd-ls-projects.html[gerrit ls-projects]::
 	List projects visible to the caller.
 
@@ -97,7 +100,7 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 link:cmd-create-account.html[gerrit create-account]::
-	Create a new batch/role account.
+	Create a new user account.
 
 link:cmd-set-account.html[gerrit set-account]::
 	Change an account's settings.
@@ -120,6 +123,9 @@
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
+link:cmd-set-members.html[gerrit set-members]::
+	Set group members.
+
 link:cmd-set-project-parent.html[gerrit set-project-parent]::
 	Change the project permissions are inherited from.
 
diff --git a/Documentation/cmd-ls-members.txt b/Documentation/cmd-ls-members.txt
new file mode 100644
index 0000000..9814ff2
--- /dev/null
+++ b/Documentation/cmd-ls-members.txt
@@ -0,0 +1,64 @@
+gerrit ls-members
+================
+
+NAME
+----
+gerrit ls-members - Show members of a given group
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ls-members GROUPNAME'
+  [--recursive]
+
+DESCRIPTION
+-----------
+Displays the members of the given group, one per line, so long as the given
+group is visible to the user. The users' id, username, full name and email are
+shown tab-separated.
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts. Output is either an error
+message or a heading followed by zero or more lines, one for each member of the
+group. If any field is not set, or if the field is the user's full name and the
+name is empty, "n/a" is emitted as the field value.
+
+All non-printable characters (ASCII value 31 or less) are escaped
+according to the conventions used in languages like C, Python, and Perl,
+employing standard sequences like `\n` and `\t`, and `\xNN` for all
+others. In shell scripts, the `printf` command can be used to unescape
+the output.
+
+OPTIONS
+-------
+--recursive::
+	If a member of the group is itself a group, the sub-group's
+	members are included in the list. Otherwise members of any sub-group
+	are not shown and no indication is given that a sub-group is present
+
+EXAMPLES
+--------
+
+List members of the Administrators group:
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-members Administrators
+	id      username  full name    email
+	100000  jim     Jim Bob somebody@example.com
+	100001  johnny  John Smith      n/a
+	100002  mrnoname        n/a     someoneelse@example.com
+=====
+
+List members of a non-existent group:
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-members BadlySpelledGroup
+	Group not found or not visible
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-install.txt b/Documentation/cmd-plugin-install.txt
index 79d1f4a..719c2bc 100644
--- a/Documentation/cmd-plugin-install.txt
+++ b/Documentation/cmd-plugin-install.txt
@@ -41,7 +41,9 @@
 
 --name::
 -n::
-	The name under which the plugin should be installed.
+	The name under which the plugin should be installed. Note: if the plugin
+	provides its own name in the MANIFEST file, then the plugin name from the
+	MANIFEST file has precedence over this option.
 
 EXAMPLES
 --------
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 65c21db..70213da 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -11,7 +11,6 @@
 'ssh' -p <port> <host> 'gerrit review'
   [--project <PROJECT> | -p <PROJECT>]
   [--message <MESSAGE> | -m <MESSAGE>]
-  [--force-message]
   [--submit | -s]
   [--abandon | --restore]
   [--publish]
@@ -51,22 +50,6 @@
 	Optional cover letter to include as part of the message
 	sent to reviewers when the approval states are updated.
 
---force-message::
-	Option which allows Gerrit to publish the --message, even
-	when the labels could not be applied due to the change being
-	closed.
-+
-Used by some scripts/CI-systems, where the results (or links
-to the result) are posted as a message after completion of a
-build (often together with a label-change, indicating the success
-of the build).
-+
-If the message is posted successfully, the command will return
-successfully, even if the label could not be changed.
-+
-This option will not force the message to be posted if the command
-fails because the user is not permitted to change the label.
-
 --help::
 -h::
 	Display site-specific usage information, including the
diff --git a/Documentation/cmd-set-members.txt b/Documentation/cmd-set-members.txt
new file mode 100644
index 0000000..7524893
--- /dev/null
+++ b/Documentation/cmd-set-members.txt
@@ -0,0 +1,82 @@
+gerrit set-members
+==================
+
+NAME
+----
+gerrit set-members - Set group members
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit set-members'
+  [--add USER ...]
+  [--remove USER ...]
+  [--include GROUP ...]
+  [--exclude GROUP ...]
+  [--]
+  <GROUP> ...
+
+DESCRIPTION
+-----------
+Set the group members for the specified groups.
+
+OPTIONS
+-------
+<GROUP>::
+	Required; name of the group for which the members should be set.
+	The members for multiple groups can be set at once by specifying
+	multiple groups.
+
+--add::
+-a::
+	A user that should be added to the specified groups. Multiple
+	users can be added at once by using this option multiple times.
+
+--remove::
+-r::
+	Remove this user from the specified groups. Multiple users can be
+	removed at once by using this option multiple times.
+
+--include::
+-i::
+	A group that should be included to the specified groups. Multiple
+	groups can be included at once by using this option multiple
+	times.
+
+--exclude::
+-e::
+	Exclude this group from the specified groups. Multiple groups can
+	be excluded at once by using this option multiple times.
+
+The `set-members` command is processing the options in the following
+order: `--remove`, `--exclude`, `--add`, `--include`
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+Add alice and bob, but remove eve from the groups my-committers and
+my-verifiers.
+=====
+	$ ssh -p 29418 review.example.com gerrit set-members \
+	  -a alice@example.com -a bob@example.com \
+	  -r eve@example.com my-committers my-verifiers
+=====
+
+Include the group my-friends into the group my-committers, but
+exclude the included group my-testers from the group my-committers.
+=====
+	$ ssh -p 29418 review.example.com gerrit set-members \
+	  -i my-friends -e my-testers my-committers
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
index a0af910..af20006 100644
--- a/Documentation/cmd-set-project.txt
+++ b/Documentation/cmd-set-project.txt
@@ -16,6 +16,7 @@
   [--content-merge <true|false|inherit>]
   [--change-id <true|false|inherit>]
   [--project-state <STATE> | --ps <STATE>]
+  [--max-object-size-limit <N>]
   <NAME>
 
 DESCRIPTION
@@ -56,6 +57,7 @@
 +
 * FAST_FORWARD_ONLY: produces a strictly linear history.
 * MERGE_IF_NECESSARY: create a merge commit when required.
+* REBASE_IF_NECESSARY: rebase the commit when required.
 * MERGE_ALWAYS: always create a merge commit.
 * CHERRY_PICK: always cherry-pick the commit.
 
@@ -93,6 +95,15 @@
 is granted, but all modification operations are disabled.
 * HIDDEN: the project is not visible for those who are not owners
 
+--max-object-size-limit::
+	Define maximum Git object size for this project. Pushes containing an
+	object larger than this limit will be rejected. This can be used to
+	further limit the global
+  link:config-gerrit.html#receive.maxObjectSizeLimit[receive.maxObjectSizeLimit]
+	and cannot be used to increase that globally set limit.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
 EXAMPLES
 --------
 Change project `example` to be hidden, require change id, don't use content merge
@@ -105,4 +116,4 @@
 
 GERRIT
 ------
-Part of link:index.html[Gerrit Code Review]
\ No newline at end of file
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index 126b2a0..d426508 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -25,6 +25,10 @@
 	operating system, and other details about the environment
 	that Gerrit Code Review is running in.
 
+--width::
+-w::
+	Width of the output table.
+
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index 8404a97..ab9fadf 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -32,6 +32,11 @@
 -n::
 	Show client hostnames as IP addresses instead of DNS hostname.
 
+--wide::
+-w::
+	Do not format the output to the terminal width (default of
+	80 columns).
+
 DISPLAY
 -------
 
diff --git a/Documentation/cmd-show-queue.txt b/Documentation/cmd-show-queue.txt
index f99e342..4ab3097 100644
--- a/Documentation/cmd-show-queue.txt
+++ b/Documentation/cmd-show-queue.txt
@@ -18,7 +18,7 @@
 Gerrit contains an internal scheduler, similar to cron, that it
 uses to queue and dispatch both short and long term activity.
 
-Tasks that are completed or cancelled exit the queue very quickly
+Tasks that are completed or canceled exit the queue very quickly
 once they enter this state, but it can be possible to observe tasks
 in these states.
 
@@ -37,6 +37,13 @@
 ---------
 Intended for interactive use only.
 
+OPTIONS
+-------
+--wide::
+-w::
+	Do not format the output to the terminal width (default of
+	80 columns).
+
 DISPLAY
 -------
 
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 6da0ef0..2a3265e 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -14,7 +14,7 @@
 -----------
 
 Provides a portal into the major events occurring on the server,
-outputing activity data in real-time to the client.  Events are
+outputting activity data in real-time to the client.  Events are
 filtered by the caller's access permissions, ensuring the caller
 only receives events for changes they can view on the web, or in
 the project repository.
@@ -52,6 +52,7 @@
 Note that any field may be missing in the JSON messages, so consumers of
 this JSON stream should deal with that appropriately.
 
+[[events]]
 Events
 ~~~~~~
 Patchset Created
@@ -70,7 +71,7 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchSet[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 uploader:: link:json.html#account[account attribute]
 
@@ -148,10 +149,19 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchSet[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 reviewer:: link:json.html#account[account attribute]
 
+Topic Changed
+^^^^^^^^^^^^^
+type:: "topic-changed"
+
+change:: link:json.html#change[change attribute]
+
+changer:: link:json.html#account[account attribute]
+
+oldTopic:: Topic name before it was changed.
 
 SEE ALSO
 --------
diff --git a/Documentation/cmd-suexec.txt b/Documentation/cmd-suexec.txt
index baffd53..78fc361 100644
--- a/Documentation/cmd-suexec.txt
+++ b/Documentation/cmd-suexec.txt
@@ -19,10 +19,14 @@
 
 DESCRIPTION
 -----------
-The suexec command can only be invoked by the magic user `Gerrit
-Code Review` and permits executing any other command as any other
+The suexec command permits executing any other command as any other
 registered user account.
 
+suexec can only be invoked by the magic user `Gerrit Code Review`,
+or any user granted granted the link:access-control.html#capability_runAs[Run As]
+capability. The run as capability is permitted to be used only if
+link:config-gerrit.html[auth.enableRunAs] is true.
+
 OPTIONS
 -------
 
@@ -39,7 +43,8 @@
 ACCESS
 ------
 Caller must be the magic user Gerrit Code Review using the SSH
-daemon's host key or a key on this daemon's peer host key ring.
+daemon's host key, or a key on this daemon's peer host key ring,
+or a user granted the Run As capability.
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-version.txt b/Documentation/cmd-version.txt
index d1f94ea..aa08848 100644
--- a/Documentation/cmd-version.txt
+++ b/Documentation/cmd-version.txt
@@ -1,5 +1,5 @@
 gerrit version
-================
+==============
 
 NAME
 ----
diff --git a/Documentation/config-auto-site-initialization.txt b/Documentation/config-auto-site-initialization.txt
new file mode 100644
index 0000000..4c204fd
--- /dev/null
+++ b/Documentation/config-auto-site-initialization.txt
@@ -0,0 +1,82 @@
+Gerrit Code Review - Automatic Site Initialization on Startup
+=============================================================
+
+Description
+-----------
+
+Gerrit supports automatic site initialization on server startup
+when Gerrit runs in a servlet container. Both creation of a new site
+and upgrade of an existing site are supported. Installation of
+plugins during the site creation/initialization is not yet supported.
+
+This feature may be useful for such setups where Gerrit administrators
+don't have direct access to the database and the file system of the
+server where Gerrit should be deployed and, therefore, cannot perform
+the init from their local machine prior to deploying Gerrit on such a
+server. It may also make deployment and testing in a local servlet
+container faster to setup as the init step could be skipped.
+
+Gerrit Configuration
+--------------------
+
+The site initialization will be performed only if the `gerrit.init`
+system property exists (the value of the property is not used, only the
+existence of the property matters).
+
+If the `gerrit.site_path` system property is defined then the init is
+run for that site. The database connectivity, in that case, is defined
+in the `etc/gerrit.config`.
+
+If `gerrit.site_path` is not defined then Gerrit will try to find an
+existing site by looking into the `system_config` table in the database
+defined via the `jdbc/ReviewDb` JNDI property. If the `system_config`
+table exists then the `site_path` from that table is used for the
+initialization. The database connectivity is defined by the
+`jdbc/ReviewDb` JNDI property.
+
+Finally, if neither the `gerrit.site_path` property nor the
+`system_config` table exists, the `gerrit.init_path` system property,
+if defined, will be used to determine the site path. The database
+connectivity, also for this case, is defined by the `jdbc/ReviewDb`
+JNDI property.
+
+Example 1
+~~~~~~~~~
+
+Prepare Tomcat so that a site is initialized at a given path using
+the H2 database (if the site doesn't exist yet) or using whatever
+database is defined in `etc/gerrit.config` of that site:
+
+----
+  $ export CATALINA_OPTS='-Dgerrit.init -Dgerrit.site_path=/path/to/site'
+  $ catalina.sh start
+----
+
+Example 2
+~~~~~~~~~
+
+Prepare Tomcat so that an existing site with the path defined in the
+`system_config` table is initialized (upgraded) on Gerrit startup. The
+assumption is that the `jdbc/ReviewDb` JNDI property is defined in
+Tomcat:
+
+----
+  $ export CATALINA_OPTS='-Dgerrit.init'
+  $ catalina.sh start
+----
+
+Example 3
+~~~~~~~~~
+
+Assuming the database schema doesn't exist in the database defined
+via the `jdbc/ReviewDb` JNDI property, initialize a new site using that
+database and a given path:
+
+----
+  $ export CATALINA_OPTS='-Dgerrit.init -Dgerrit.init_path=/path/to/site'
+  $ catalina.sh start
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 93492ba..6d00dfa 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -119,6 +119,8 @@
 Therefore, the "_LDAP" suffix in the name of this authentication type.
 This authentication type can only be used under hosted daemon mode, and
 the httpd.listenUrl must use https:// as the protocol.
+Optionally, certificate revocation list file can be used
+at <review-site>/etc/crl.pem. For details, see httpd.sslCrl.
 +
 * `LDAP`
 +
@@ -140,8 +142,8 @@
 <<ldap.server,ldap.server>>.  In this configuration the web server
 is not involved in the user authentication process.
 +
-Unlike LDAP above, the username used to perform the LDAP simple bind
-request is the exact string supplied by in the dialog by the user.
+Unlike `LDAP` above, the username used to perform the LDAP simple bind
+request is the exact string supplied in the dialog by the user.
 The configured <<ldap.username,ldap.username>> identity is not used to obtain
 account information.
 +
@@ -165,7 +167,7 @@
 +
 List of permitted OpenID providers.  A user may only authenticate
 with an OpenID that matches this list.  Only used if `auth.type`
-is set to OpenID (the default).
+is set to `OpenID` (the default).
 +
 Patterns may be either a
 link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard
@@ -178,7 +180,7 @@
 [[auth.trustedOpenID]]auth.trustedOpenID::
 +
 List of trusted OpenID providers.  Only used if `auth.type` is
-set to OpenID (the default).
+set to `OpenID` (the default).
 +
 In order for a user to take advantage of permissions beyond those
 granted to the `Anonymous Users` and `Registered Users` groups,
@@ -196,7 +198,7 @@
 [[auth.openIdDomain]]auth.openIdDomain::
 +
 List of allowed OpenID email address domains. Only used if
-`auth.type` is set to "OPENID" or "OPENID_SSO".
+`auth.type` is set to `OPENID` or `OPENID_SSO`.
 +
 Domain is case insensitive and must be in the same form as it
 appears in the email address, for example, "example.com".
@@ -245,15 +247,61 @@
 
 [[auth.openIdSsoUrl]]auth.openIdSsoUrl::
 +
-The SSO entry point URL.  Only used if `auth.type` was set to
-OpenID_SSO.
+The SSO entry point URL.  Only used if `auth.type` is set to
+`OpenID_SSO`.
 +
 The "Sign In" link will send users directly to this URL.
 
 [[auth.httpHeader]]auth.httpHeader::
 +
 HTTP header to trust the username from, or unset to select HTTP basic
-or digest authentication.  Only used if `auth.type` is set to HTTP.
+or digest authentication.  Only used if `auth.type` is set to `HTTP`.
+
+[[auth.httpDisplaynameHeader]]auth.httpDisplaynameHeader::
++
+HTTP header to retrieve the user's display name from.  Only used if `auth.type`
+is set to `HTTP`.
++
+If set, Gerrit trusts and enforces the user's full name using the HTTP header
+and disables the ability to manually modify the user's full name
+from the contact information page.
+
+[[auth.httpEmailHeader]]auth.httpEmailHeader::
++
+HTTP header to retrieve the user's e-mail from.  Only used if `auth.type`
+is set to `HTTP`.
++
+If set, Gerrit trusts and enforces the user's e-mail using the HTTP header
+and disables the ability to manually modify or register other e-mails
+from the contact information page.
+
+[[auth.loginUrl]]auth.loginUrl::
++
+URL to redirect a browser to after the end-user has clicked on the
+login link in the upper right corner. Only used if `auth.type` is set
+to `HTTP` or `HTTP_LDAP`.
+Organizations using an enterprise single-sign-on solution may want to
+redirect the browser to the SSO product's sign-in page for completing the
+login process and validate their credentials.
++
+If set, Gerrit allows anonymous access until the end-user performs the login
+and provides a trusted identity through the HTTP header.
+If not set, Gerrit requires the HTTP header with a trusted identity
+and returns the error page 'LoginRedirect.html' if such a header is not
+present.
+
+[[auth.loginText]]auth.loginText::
++
+Text displayed in the loginUrl link. Only used if `auth.loginUrl` is set.
++
+If not set, the "Sign In" text is used.
+
+[[auth.registerPageUrl]]auth.registerPageUrl::
++
+URL of the registration page to use when a new user logs in to Gerrit for
+the first time. Used only when `auth.type` is set to `HTTP`.
++
+If not set, the standard Gerrit registration page `/#/register/` is displayed.
 
 [[auth.logoutUrl]]auth.logoutUrl::
 +
@@ -267,14 +315,14 @@
 [[auth.registerUrl]]auth.registerUrl::
 +
 Target for the "Register" link in the upper right corner.  Used only
-when auth.type is `LDAP`.
+when `auth.type` is `LDAP`.
 +
 If not set, no "Register" link is displayed.
 
 [[auth.registerText]]auth.registerText::
 +
 Text for the "Register" link in the upper right corner.  Used only
-when auth.type is `LDAP`.
+when `auth.type` is `LDAP`.
 +
 If not set, defaults to "Register".
 
@@ -285,9 +333,19 @@
 
 [[auth.httpPasswordUrl]]auth.httpPasswordUrl::
 +
-Target for the "Obtain Password" link.  Used only when auth.type is
+Target for the "Obtain Password" link.  Used only when `auth.type` is
 `LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
+
+[[auth.switchAccountUrl]]auth.switchAccountUrl::
 +
+URL to switch user identities and login as a different account than
+the currently active account.  This is disabled by default except when
+`auth.type` is `OPENID` and `DEVELOPMENT_BECOME_ANY_ACCOUNT`.  If set
+the "Switch Account" link is displayed next to "Sign Out".
++
+When `auth.type` does not normally enable this URL administrators may
+set this to `login/` or `$canonicalWebUrl/login`, allowing users to
+begin a new web session.
 
 [[auth.cookiePath]]auth.cookiePath::
 +
@@ -305,7 +363,7 @@
 [[auth.emailFormat]]auth.emailFormat::
 +
 Optional format string to construct user email addresses out of
-user login names.  Only used if auth.type is `HTTP`, `HTTP_LDAP`
+user login names.  Only used if `auth.type` is `HTTP`, `HTTP_LDAP`
 or `LDAP`.
 +
 This value can be set to a format string, where `{0}` is replaced
@@ -330,6 +388,10 @@
 more agreements.
 +
 By default this is false (no agreements are used).
++
+To enable the actual usage of contributor agreement the project
+specific config option in the `project.config` must be set:
+link:config-project-config.html[receive.requireContributorAgreement].
 
 auth.allowGoogleAccountUpgrade::
 +
@@ -390,6 +452,18 @@
 +
 By default this is set to false.
 
+[[auth.enableRunAs]]auth.enableRunAs::
++
+If true HTTP REST APIs will accept the `X-Gerrit-RunAs` HTTP request
+header from any users granted the link:access-control.html#capability_runAs[Run As]
+capability. The header and capability permit the authenticated user
+to impersonate another account.
++
+If false the feature is disabled and cannot be re-enabled without
+editing gerrit.config and restarting the server.
++
+Default is true.
+
 [[cache]]Section cache
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -497,7 +571,7 @@
 cache `"adv_bases"`::
 +
 Used only for push over smart HTTP when branch level access controls
-are enabled.  The cache entry contains all commits that are avaliable
+are enabled.  The cache entry contains all commits that are available
 for the client to use as potential delta bases.  Push over smart HTTP
 requires two HTTP requests, and this cache tries to carry state from
 the first request into the second to ensure it can complete.
@@ -671,7 +745,7 @@
 Boolean to enable or disable the computation of intraline differences
 when populating a diff cache entry.  This flag is provided primarily
 as a backdoor to disable the intraline difference feature if
-necessary.  To maintain backwards compatability with prior versions,
+necessary.  To maintain backwards compatibility with prior versions,
 this setting will fallback to `cache.diff.intraline` if not set in the
 configuration.
 +
@@ -693,6 +767,28 @@
 +
 Default is 5 minutes.
 
+[[change]]Section change
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[change.updateDelay]]change.updateDelay::
++
+How often in seconds the web interface should poll for updates to the
+currently open change.  The poller relies on the client's browser
+cache to use If-Modified-Since and respect `304 Not Modified` HTTP
+reponses.  This allows for fast polls, often under 8 milliseconds.
++
+With a configured 30 second delay a server with 4900 active users will
+typically need to dedicate 1 CPU to the update check.  4900 users
+divided by an average delay of 30 seconds is 163 requests arriving per
+second.  If requests are served at ~6 ms response time, 1 CPU is
+necessary to keep up with the update request traffic.  On a smaller
+user base of 500 active users, the default 30 second delay is only 17
+requests per second and requires ~10% CPU.
++
+If 0 the update polling is disabled.
++
+Default is 30 seconds.
+
 [[changeMerge]]Section changeMerge
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -843,6 +939,13 @@
 values are configured, they are passed in order on the command line,
 separated by spaces.  These options are appended onto 'JAVA_OPTIONS'.
 
+For example, it is possible to overwrite Gerrit's default log4j
+configuration:
+
+----
+  javaOptions = -Dlog4j.configuration=file:///home/gerrit/site/etc/log4j.properties
+----
+
 [[container.slave]]container.slave::
 +
 Used on Gerrit slave installations. If set to true the Gerrit JVM is
@@ -923,7 +1026,7 @@
 Largest object size, in bytes, that JGit will allocate as a
 contiguous byte array.  Any file revision larger than this threshold
 will have to be streamed, typically requiring the use of temporary
-files under '$GIT_DIR/objects' to implement psuedo-random access
+files under '$GIT_DIR/objects' to implement pseudo-random access
 during delta decompression.
 +
 Servers with very high traffic should set this to be larger than
@@ -948,7 +1051,7 @@
 can be made by the JVM native code.
 +
 In server applications (such as Gerrit) that need to access many
-pack files, setting this to true risks artifically running out
+pack files, setting this to true risks artificially running out
 of virtual address space, as the garbage collector cannot reclaim
 unused mapped spaces fast enough.
 +
@@ -1235,6 +1338,20 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
+[[gerrit.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
++
+Optional command to install the `commit-msg` hook. Typically of the
+form:
+----
+fetch-cmd some://url/to/commit-msg .git/hooks/commit-msg ; chmod +x .git/hooks/commit-msg
+----
+
++
+By default unset; falls back to using scp from the canonical SSH host,
+or curl from the canonical HTTP URL for the server.  Only necessary if a
+proxy or other server/network configuration prevents clients from
+fetching from the default location.
+
 [[gerrit.gitHttpUrl]]gerrit.gitHttpUrl::
 +
 Optional base URL for repositories available over the HTTP
@@ -1252,6 +1369,11 @@
 Code Review's own bug tracker but could be directed to the system
 administrator's ticket queue.
 
+[[gerrit.changeScreen]]gerrit.changeScreen::
++
+Default change screen UI to direct users to. Valid values are
+`OLD_UI` and `CHANGE_SCREEN2`. Default is `OLD_UI`.
+
 [[gitweb]]Section gitweb
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1342,6 +1464,19 @@
 +
 Valid values are the characters '*', '(' and ')'.
 
+[[gitweb.linkDrafts]]gitweb.urlEncode::
++
+Whether or not Gerrit should encode the generated viewer URL.
++
+Gerrit composes the viewer URL using information about the project, branch, file
+or commit of the target object to be displayed. Typically viewers such as CGit
+and GitWeb do need those parts to be encoded, including the '/' in project's name,
+for being correctly parsed.
+However other viewers could instead require an unencoded URL (e.g. GitHub web
+based viewer)
++
+Valid values are "true" and "false," default is "true."
+
 [[gitweb.linkDrafts]]gitweb.linkDrafts::
 +
 Whether or not Gerrit should provide links to gitweb on draft patch sets.
@@ -1350,7 +1485,7 @@
 only allows publicly viewable references, set this to false to remove
 the links to draft patch sets from the change review screen.
 +
-Valid values are "true" and "false," default is "true."
+Valid values are "true" and "false," default is "true".
 
 [[groups]]Section groups
 ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1416,6 +1551,11 @@
 Optional filename for the reviewer added hook, if not specified then
 `reviewer-added` will be used.
 
+[[hooks.topicChangedHook]]hooks.topicChangedHook::
++
+Optional filename for the topic changed hook, if not specified then
+`topic-changed` will be used.
+
 [[hooks.claSignedHook]]hooks.claSignedHook::
 +
 Optional filename for the CLA signed hook, if not specified then
@@ -1535,6 +1675,23 @@
 By default, 16384 (16 K), which is sufficient for most OpenID and
 other web-based single-sign-on integrations.
 
+[[httpd.sslCrl]]httpd.sslCrl::
++
+Path of the certificate revocation list file in PEM format. This
+crl file is optional, and available for CLIENT_SSL_CERT_LDAP
+authentication.
++
+To create and view a crl using openssl:
++
+----
+openssl ca -gencrl -out crl.pem
+openssl crl -in crl.pem -text
+----
++
+If not absolute, the path is resolved relative to `$site_path`.
++
+By default, `$site_path/etc/crl.pem`.
+
 [[httpd.sslKeyStore]]httpd.sslKeyStore::
 +
 Path of the Java keystore containing the server's SSL certificate
@@ -1582,7 +1739,7 @@
 +
 Minimum number of spare threads to keep in the worker thread pool.
 This number must be at least 1 larger than httpd.acceptorThreads
-multipled by the number of httpd.listenUrls configured.
+multiplied by the number of httpd.listenUrls configured.
 +
 By default, 5, suitable for most lower-volume traffic sites.
 
@@ -1623,13 +1780,81 @@
 +
 By default, 5 minutes.
 
+[[httpd.filterClass]]httpd.filterClass::
++
+Class that implements the javax.servlet.Filter interface
+for filtering any HTTP related traffic going through the Gerrit
+HTTP protocol.
+Class is loaded and configured in the Gerrit Jetty container
+and run in front of all Gerrit URL handlers, allowing the filter
+to inspect, modify, allow or reject each request.
+It needs to be provided as JAR library
+under $GERRIT_SITE/lib as it is resolved using the default Gerrit class
+loader and cannot be dynamically loaded by a plugin.
++
+Failing to load the Filter class would result in a Gerrit start-up
+failure, as this class is supposed to provide mandatory filtering
+in front of Gerrit HTTP protocol.
++
+Typical usage is in conjunction with the `auth.type=HTTP` as replacement
+of an Apache HTTP proxy layer as security enforcement on top of Gerrit
+by returning a trusted username as HTTP Header.
++
+Example of using a security library secure.jar under $GERRIT_SITE/lib
+that provides a org.anyorg.MySecureFilter Servlet Filter that enforces
+a trusted username in the `TRUSTED_USER` HTTP Header:
+
+----
+[auth]
+	type = HTTP
+	httpHeader = TRUSTED_USER
+
+[http]
+	filterClass = org.anyorg.MySecureFilter
+----
+
+[[httpd.robotsFile]]httpd.robotsFile::
++
+Location of an external robots.txt file to be used instead of the one
+bundled with the .war of the application.
++
+If not absolute, the path is resolved relative to `$site_path`.
++
+If the file doesn't exist or can't be read the default robots.txt file
+bundled with the .war will be used instead.
+
+[[index]]Section index
+~~~~~~~~~~~~~~~~~~~~~~
+
+The index section configures the secondary index.
+
+[[index.type]]index.type::
++
+Type of secondary indexing employed by Gerrit.  The supported
+values are:
++
+* `LUCENE`
++
+A link:http://lucene.apache.org/[Lucene] index is used.
++
+* `SOLR`
++
+A link:http://lucene.apache.org/solr/[Solr] index is used.
++
+* `SQL`
++
+No secondary index.  Not all query operators are supported.  Other
+query operators are routed through the standard SQL query engine.
+
++
+By default, `SQL`.
 
 [[ldap]]Section ldap
 ~~~~~~~~~~~~~~~~~~~~
 
 LDAP integration is only enabled if `auth.type` is set to
 `HTTP_LDAP`, `LDAP` or `CLIENT_SSL_CERT_LDAP`.  See above for a
-detailed description of the auth.type settings and their
+detailed description of the `auth.type` settings and their
 implications.
 
 An example LDAP configuration follows, and then discussion of
@@ -1658,7 +1883,7 @@
 and group membership from.  Must be of the form `ldap://host` or
 `ldaps://host` to bind with either a plaintext or SSL connection.
 +
-If auth.type is `LDAP` this setting should use `ldaps://` to
+If `auth.type` is `LDAP` this setting should use `ldaps://` to
 ensure the end user's plaintext password is transmitted only over
 an encrypted connection.
 
@@ -1720,9 +1945,9 @@
 +
 Query pattern to use when searching for a user account.  This may be
 any valid LDAP query expression, including the standard `(&...)` and
-`(|...)` operators.  If auth.type is `HTTP_LDAP` then the variable
+`(|...)` operators.  If `auth.type` is `HTTP_LDAP` then the variable
 `${username}` is replaced with a parameter set to the username
-that was supplied by the HTTP server.  If auth.type is `LDAP` then
+that was supplied by the HTTP server.  If `auth.type` is `LDAP` then
 the variable `${username}` is replaced by the string entered by
 the end user.
 +
@@ -1834,7 +2059,7 @@
 account is currently a member of.  This may be any valid LDAP query
 expression, including the standard `(&...)` and `(|...)` operators.
 +
-If auth.type is `HTTP_LDAP` then the variable `${username}` is
+If `auth.type` is `HTTP_LDAP` then the variable `${username}` is
 replaced with a parameter set to the username that was supplied
 by the HTTP server.  Other variables appearing in the pattern,
 such as `${fooBarAttribute}`, are replaced with the value of the
@@ -1905,7 +2130,7 @@
 Note the `renewTGT` property to make sure the TGT does not expire,
 and `useTicketCache` to use the TGT supplied by the operating system. As
 the whole point of using GSSAPI is to have passwordless authentication
-to the LDAP service, this option does not aquire a new TGT on its own.
+to the LDAP service, this option does not acquire a new TGT on its own.
 
 On Windows servers the registry key `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters`
 must have the DWORD value `allowtgtsessionkey` set to 1 and the account must not
@@ -2042,6 +2267,11 @@
 Gerrit administrators can use this setting to prevent developers
 from pushing objects which are too large to Gerrit.
 +
+This setting can also be set in the `project.config`
+link:config-project-config.html[receive.maxObjectSizeLimit] in order
+to further reduce the global setting. The project specific setting is
+only honored when it further reduces the global limit.
++
 Default is zero.
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
@@ -2062,7 +2292,7 @@
 set update.
 +
 Defaults to 1, using only the main receive thread. This feature is for
-databases with very high latency that can benfit from concurrent
+databases with very high latency that can benefit from concurrent
 operations when multiple changes are impacted at once.
 
 [[receive.timeout]]receive.timeout::
@@ -2073,7 +2303,7 @@
 be specified using standard time unit abbreviations ('ms', 'sec',
 'min', etc.).
 +
-Default is 2 minutes. If no unit is specified, millisconds
+Default is 2 minutes. If no unit is specified, milliseconds
 is assumed.
 
 
@@ -2128,7 +2358,7 @@
 * `USER`
 +
 Gerrit will set the From header to use the current user's
-Full Name and Preferred Email.  This may cause messsages to be
+Full Name and Preferred Email.  This may cause messages to be
 classified as spam if the user's domain has SPF or DKIM enabled
 and <<sendemail.smtpServer,sendemail.smtpServer>> is not a trusted
 relay for that domain.
@@ -2262,28 +2492,6 @@
 and text results for changes. If false, the URL is disabled and
 returns 404 to clients. Default is true, enabling `/query`.
 
-[[site.upgradeSchemaOnStartup]]site.upgradeSchemaOnStartup::
-+
-Control whether schema upgrade should be done on Gerrit startup. The following
-values are supported:
-+
-* `OFF`
-+
-No automatic schema upgrade on startup.
-+
-* `AUTO`
-+
-Perform schema migration on startup, if necessary.  If, as a result of
-schema migration, there would be any unused database objects they will
-be dropped automatically.
-+
-* `AUTO_NO_PRUNE`
-+
-Like `AUTO` but unused database objects will not be pruned.
-
-+
-The default is `OFF`.
-
 [[ssh-alias]] Section ssh-alias
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2376,7 +2584,7 @@
 +
 If the number of threads requested for non-interactive users is larger
 than the total number of threads allocated in sshd.threads, then the
-value of sshd.threads is increased to accomodate the requested value.
+value of sshd.threads is increased to accommodate the requested value.
 +
 By default, 0.
 
@@ -2498,6 +2706,13 @@
 +
 By default, `host/canonical.host.name`
 
+[[sshd.requestLog]]sshd.requestLog::
++
+Enable (or disable) the `'$site_path'/logs/sshd_log` request log.
+If enabled, a request log file is written out by the SSH daemon.
++
+By default, true.
+
 [[suggest]] Section suggest
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2609,14 +2824,32 @@
   backgroundColor = 00FFFF
 ----
 
+As example, here is the theme configuration to have the old green look:
+
+----
+[theme]
+  backgroundColor = FCFEEF
+  textColor = 000000
+  trimColor = D4E9A9
+  selectionColor = FFFFCC
+  topMenuColor = D4E9A9
+  changeTableOutdatedColor = F08080
+[theme "signed-in"]
+  backgroundColor = FFFFFF
+----
+
 [[trackingid]] Section trackingid
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Tagged footer lines containing references to external
 tracking systems, parsed out of the commit message and
-saved in Gerrit's database.  After making changes to
-this section, existing changes must be reindexed with the
-link:pgm-ScanTrackingIds.html[ScanTrackingIds] program.
+saved in Gerrit's database.
+
+After making changes to this section, existing changes
+must be reindexed with link:pgm-reindex.html[reindex]
+if index.type is `LUCENE` or `SOLR`; or with
+link:pgm-ScanTrackingIds.html[ScanTrackingIds] if index.type
+is unset or `SQL`.
 
 The tracking ids are searchable using tr:<tracking id> or
 bug:<tracking id>.
@@ -2722,7 +2955,7 @@
 
 [[user.anonymousCoward]]user.anonymousCoward::
 +
-Username that this displayed in the Gerrit WebUI and in e-mail
+Username that is displayed in the Gerrit WebUI and in e-mail
 notifications if the full name of the user is not set.
 +
 By default "Anonymous Coward" is used.
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index e5edda8..7ba15b8 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -116,7 +116,7 @@
 # logo to use
 $logo = "git-logo.png";
 
-# the ‘favicon’
+# the favicon
 $favicon = "git-favicon.png";
 ----
 
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 1236077..5875837 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -90,7 +90,7 @@
 Called whenever a change has been abandoned.
 
 ====
-  change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --reason <reason>
+  change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --commit <sha1> --reason <reason>
 ====
 
 change-restored
@@ -99,7 +99,7 @@
 Called whenever a change has been restored.
 
 ====
-  change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --reason <reason>
+  change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --commit <sha1> --reason <reason>
 ====
 
 ref-updated
@@ -120,6 +120,15 @@
   reviewer-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --reviewer <reviewer>
 ====
 
+topic-changed
+~~~~~~~~~~~~~
+
+Called whenever a change's topic is changed from the Web UI or via the REST API.
+
+====
+  topic-changed --change <change id> --project <project name> --branch <branch> --changer <changer> --old-topic <old topic> --new-topic <new topic>
+====
+
 cla-signed
 ~~~~~~~~~~
 
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 1ff7e24..c08d484 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -177,7 +177,7 @@
 
 [[label_abbreviation]]
 `label.Label-Name.abbreviation`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 An abbreviated name for a label shown as a compact column header, for
 example on project dashboards. Defaults to all the uppercase characters
@@ -203,6 +203,12 @@
 must be at least one positive value, or else submit will never be
 enabled. To permit blocking submits, ensure a negative value is defined.
 
+* `AnyWithBlock`
++
+The lowest possible negative value, if present, blocks a submit, Any
+other value enables a submit. To permit blocking submits, ensure
+that a negative value is defined.
+
 * `MaxNoBlock`
 +
 The highest possible positive value is required to enable submit, but
@@ -230,6 +236,30 @@
 sticky approvals, reducing turn-around for trivial cleanups prior to
 submitting a change.
 
+[[label_copyAllScoresOnTrivialRebase]]
+`label.Label-Name.copyAllScoresOnTrivialRebase`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If true, all scores for the label are copied forward when a new patch
+set is uploaded that is a trivial rebase. A new patch set is considered
+as trivial rebase if the commit message is the same as in the previous
+patch set and if it has the same code delta as the previous patch set.
+This is the case if the change was rebased onto a different parent.
+This can be used to enable sticky approvals, reducing turn-around for
+trivial rebases prior to submitting a change. Defaults to false.
+
+[[label_copyAllScoresIfNoCodeChange]]
+`label.Label-Name.copyAllScoresIfNoCodeChange`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If true, all scores for the label are copied forward when a new patch
+set is uploaded that has the same parent commit as the previous patch
+set and the same code delta as the previous patch set. This means only
+the commit message is different. This can be used to enable sticky
+approvals on labels that only depend on the code, reducing turn-around
+if only the commit message is changed prior to submitting a change.
+Defaults to false.
+
 [[label_canOverride]]
 `label.Label-Name.canOverride`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -238,6 +268,30 @@
 configuration for this label in child projects will be ignored. Defaults
 to true.
 
+[[label_branch]]
+`label.Label-Name.branch`
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default a given project's label applicable scope is all changes
+on all branches of this project and its child projects.
+
+Label's applicable scope can be branch specific via configuration.
+E.g. create a label `Video-Qualify` on parent project and configure
+the `branch` as:
+
+====
+  [label "Video-Qualify"]
+      branch = refs/heads/video-1.0/*
+      branch = refs/heads/video-1.1/Kino
+====
+
+Then *only* changes in above branch scope of parent project and child
+projects will be affected by `Video-Qualify`.
+
+NOTE: The `branch` is independent from the branch scope defined in `access`
+parts in `project.config` file. That means from the UI a user can always
+assign permissions for that label on a branch, but this permission is then
+ignored if the label doesn't apply for that branch.
 
 [[label_example]]
 Example
diff --git a/Documentation/config-login-register.txt b/Documentation/config-login-register.txt
index d0e0fc5..867f0d4 100644
--- a/Documentation/config-login-register.txt
+++ b/Documentation/config-login-register.txt
@@ -135,4 +135,9 @@
   git clone ssh://user@localhost:29418/REPOSITORY_NAME.git
 
   user@host:~$
-----
\ No newline at end of file
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8de8e59..3b8bffa 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -65,6 +65,13 @@
 related to a user editing the commit message through the Gerrit UI.  It is a
 `ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
 
+Footer.vm
+~~~~~~~~~
+
+The `Footer.vm` template will determine the contents of the footer text
+appended to the end of all outgoing emails after the ChangeFooter and
+CommentFooter.
+
 Merged.vm
 ~~~~~~~~~
 
@@ -83,15 +90,10 @@
 ~~~~~~~~~~~~
 
 The `NewChange.vm` template will determine the contents of the email related
-to a user submitting a new change for review. It is a `ChangeEmail`: see
-`ChangeSubject.vm` and `ChangeFooter.vm`.
-
-RebasedPatchSet.vm
-~~~~~~~~~~~~~~~~~~
-
-The `RebasedPatchSet.vm` template will determine the contents of the email
-related to a user rebasing a patchset for a change through the Gerrit UI.
-It is a `ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+to a user submitting a new change for review. This includes changes created
+by actions made by the user in the Web UI such as cherry picking a commit or
+reverting a change.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
+`ChangeFooter.vm`.
 
 RegisterNewEmail.vm
 ~~~~~~~~~~~~~~~~~~~
@@ -103,7 +105,9 @@
 ~~~~~~~~~~~~~~~~~~
 
 The `ReplacePatchSet.vm` template will determine the contents of the email
-related to a user submitting a new patchset for a change.  It is a
+related to a user submitting a new patchset for a change.  This includes
+patchsets created by actions made by the user in the Web UI such as editing
+the commit message, cherry picking a commit, or rebasing a change.  It is a
 `ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
 
 Restored.vm
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
new file mode 100644
index 0000000..8529b67
--- /dev/null
+++ b/Documentation/config-project-config.txt
@@ -0,0 +1,225 @@
+Gerrit Code Review - Project Configuration File Format
+======================================================
+
+This page explains the storage format of Gerrit's project configuration
+and access control models.
+
+The web UI access control panel is a front end for human-readable
+configuration files under the +refs/meta/config+ namespace in the
+affected project.  Direct manipulation of these files is mainly
+relevant in an automation scenario of the access controls.
+
+
+The +refs/meta/config+ namespace
+--------------------------------
+
+The namespace contains three different files that play different
+roles in the permission model.  With read permission to that reference,
+it is possible to fetch the +refs/meta/config+ reference to a local
+repository.  A nice side effect is that you can also upload changes
+to project permissions and review them just like with regular code
+changes. The preview changes option is also provided on the UI. Please note
+that you will have to configure push rights for the +refs/meta/config+ name
+space if you'd like to use the possibility to automate permission updates.
+
+
+[[file-project_config]]
+The file +project.config+
+-------------------------
+
+The +project.config+ file contains the link between groups and their
+permitted actions on reference patterns in this project and any projects
+that inherit its permissions.
+
+The format in this file corresponds to the Git config file format, so
+if you want to automate your permissions it is a good idea to use the
++git config+ command when writing to the file. This way you know you
+don't accidentally break the format of the file.
+
+Here follows a +git config+ command example:
+
+----
+$ git config -f project.config project.description "Rights inherited by all other projects"
+----
+
+Below you will find an example of the +project.config+ file format:
+
+----
+[project]
+       description = Rights inherited by all other projects
+[access "refs/*"]
+       read = group Administrators
+[capability]
+       administrateServer = group Administrators
+[receive]
+       requireContributorAgreement = false
+----
+
+As you can see, there are several sections.
+
+The link:#project-section[+project+ section] appears once per project.
+
+The link:#access-section[+access+ section] appears once per reference pattern,
+such as `refs/*` or `refs/heads/*`.  Only one access section per pattern is
+allowed.  You will find examples of keys and values in each category section
+<<access_category,below>>.
+
+The link:#receive-section[+receive+ section] appears once per project.
+
+The link:#submit-section[+submit+ section] appears once per project.
+
+The link:#capability-section[+capability+] section only appears once, and only
+in the +All-Projects+ repository.  It controls core features that are configured
+on a global level.  You can find examples of these
+<<capability_category,below>>.
+
+
+[[project-section]]
+Project section
+~~~~~~~~~~~~~~~
+
+The project section includes configuration of project settings.
+
+These are the keys:
+
+- Description
+
+
+[[receive-section]]
+Receive section
+~~~~~~~~~~~~~~~
+
+The receive section includes configuration of project-specific
+receive settings:
+
+[[receive.requireContributorAgreement]]receive.requireContributorAgreement::
++
+Controls whether or not a user must complete a contributor agreement before
+they can upload changes. Default is `INHERIT`. If `All-Project` enables this
+option then the dependent project must set it to false if users are not
+required to sign a contributor agreement prior to submitting changes for that
+specific project. To use that feature the global option in `gerrit.config`
+must be enabled:
+link:config-gerrit.html#auth.contributorAgreements[auth.contributorAgreements].
+
+[[receive.requireSignedOffBy]]receive.requireSignedOffBy::
++
+Sign-off can be a requirement for some projects (for example Linux kernel uses
+it). Sign-off is a line at the end of the commit message which certifies who
+is the author of the commit. Its main purpose is to improve tracking of who
+did  what, especially with patches. Default is `INHERIT`, which means that this
+property is inherited from the parent project.
+
+[[receive.requireChangeId]]receive.requireChangeId::
++
+Controls whether or not the Change-Id must be included in the commit message
+in the last paragraph. Default is `INHERIT`, which means that this property
+is inherited from the parent project.
+
+[[receive.maxObjectSizeLimit]]receive.maxObjectSizeLimit::
++
+Maximum allowed Git object size that receive-pack will accept. If an object
+is larger than the given size the pack-parsing will abort and the push
+operation will fail. If set to zero then there is no limit.
++
+Project owners can use this setting to prevent developers from pushing
+objects which are too large to Gerrit. This setting can also be set it
+`gerrit.config` globally link:config-gerrit.html#receive.maxObjectSizeLimit[
+receive.maxObjectSizeLimit].
++
+The project specific setting in `project.config` is only honored when it
+further reduces the global limit.
++
+Default is zero.
++
+Common unit suffixes of k, m, or g are supported.
+
+[[submit-section]]
+Submit section
+~~~~~~~~~~~~~~
+
+The submit section includes configuration of project-specific
+submit settings:
+
+- 'mergeContent': Defines whether to automatically merge changes.  Valid values
+are 'true', 'false', or 'INHERIT'.  Default is 'INHERIT'.
+
+- 'action': defines the link:project-setup.html#submit_type[submit type].  Valid
+values are 'fast forward only', 'merge if necessary', 'rebase if necessary',
+'merge always' and 'cherry pick'.  The default is 'merge if necessary'.
+
+Merge strategy
+
+
+[[access-section]]
+Access section
+~~~~~~~~~~~~~~
+
+Each +access+ section includes a reference and access rights connected
+to groups.  Each group listed must exist in the link:#file-groups[+groups+ file].
+
+Please refer to the
+link:access-control.html#access_categories[Access Categories]
+documentation for a full list of available access rights.
+
+
+[[capability-section]]
+Capability section
+~~~~~~~~~~~~~~~~~~
+
+The +capability+ section only appears once, and only in the +All-Projects+
+repository.  It controls Gerrit administration capabilities that are configured
+on a global level.
+
+Please refer to the
+link:access-control.html#global_capabilities[Global Capabilities]
+documentation for a full list of available capabilities.
+
+
+[[file-groups]]
+The file +groups+
+-----------------
+
+Each group in this list is linked with its UUID so that renaming of
+groups is possible without having to rewrite every +groups+ file
+in every repository where it's used.
+
+This is what the default groups file for +All-Projects.git+ looks like:
+
+----
+# UUID                                         Group Name
+#
+3d6da7dc4e99e6f6e5b5196e21b6f504fc530bba       Administrators
+global:Anonymous-Users                         Anonymous Users
+global:Project-Owners                          Project Owners
+global:Registered-Users                        Registered Users
+----
+
+This file can't be written to by the +git config+ command.
+
+In order to reference a group in +project.config+, it must be listed in
+the +groups+ file.  When editing permissions through the web UI this
+file is maintained automatically, but when pushing updates to
++refs/meta/config+ this must be dealt with by hand.  Gerrit will refuse
++project.config+ files that refer to groups not listed in +groups+.
+
+The UUID of a group can be found on the General tab of the group's page
+in the web UI or via the +-v+ option to
+link:cmd-ls-groups.html[the +ls-groups+ SSH command].
+
+
+[[file-rules_pl]]
+The file +rules.pl+
+-------------------
+
+The +rules.pl+ files allows you to replace or amend the default Prolog
+rules that control e.g. what conditions need to be fulfilled for a
+change to be submittable.  This file content should be
+interpretable by the 'Prolog Cafe' interpreter.
+
+You can read more about the +rules.pl+ file and the prolog rules on
+link:prolog-cookbook.html[the Prolog cookbook page].
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index ab5d04a..1b09d19 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -2,18 +2,39 @@
 ======================================
 
 Gerrit supports link:dev-plugins.html[plugin-based] validation of
-uploaded commits.
+commits.
 
-This allows plugins to perform additional validation checks against
-uploaded commits, and send back a warning or error message to the git
-client.
+[[new-commit-validation]]
+New commit validation
+---------------------
 
-To make use of this feature, a plugin must implement the `CommitValidationListener`
-interface.
+
+Plugins implementing the `CommitValidationListener` interface can
+perform additional validation checks against new commits.
+
+If the commit fails the validation, the plugin can either provide a
+message that will be sent back to the git client, or throw an exception
+which will cause the commit to be rejected.
+
+Validation applies to both commits uploaded via `git push`, and new
+commits generated via Gerrit's Web UI features such as the rebase, revert
+and cherry-pick buttons.
 
 Out of the box, Gerrit includes a plugin that checks the length of the
 subject and body lines of commit messages on uploaded commits.
 
+[[pre-merge-validation]]
+Pre-merge validation
+--------------------
+
+
+Plugins implementing the `MergeValidationListener` interface can
+perform additional validation checks against commits before they
+are merged to the git repository.
+
+If the commit fails the validation, the plugin can throw an exception
+which will cause the merge to fail.
+
 
 GERRIT
 ------
diff --git a/Documentation/config.defs b/Documentation/config.defs
new file mode 100644
index 0000000..642b915
--- /dev/null
+++ b/Documentation/config.defs
@@ -0,0 +1,20 @@
+DOCUMENTATION_DEPS = {
+  "install-quick.txt": ["config-login-register.txt"],
+  "install.txt": ["database-setup.txt"],
+}
+
+def documentation_attributes(revision):
+  return [
+    'toc',
+    'newline="\\n"',
+    'asterisk="&#42;"',
+    'plus="&#43;"',
+    'caret="&#94;"',
+    'startsb="&#91;"',
+    'endsb="&#93;"',
+    'tilde="&#126;"',
+    'last-update-label!',
+    'source-highlighter=prettify',
+    'stylesheet=doc.css',
+    'revnumber="%s"' % revision,
+  ]
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index c559b0e..3800473 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -63,3 +63,53 @@
 
 Visit MySQL's link:http://dev.mysql.com/doc/[documentation] for further
 information regarding using MySQL.
+
+[[createdb_oracle]]
+Oracle
+~~~~~~
+
+PostgreSQL or H2 is the recommended database for Gerrit Code Review.
+Oracle is supported for environments where running on an existing Oracle
+installation simplifies administrative overheads, such as database backups.
+
+Create a user for the web application within sqlplus, assign it a
+password, and grant the user full rights on the newly created database:
+
+----
+  SQL> create user gerrit2 identified by secret_password default tablespace users;
+  SQL> grant connect, resources to gerrit2;
+----
+
+JDBC driver ojdbc6.jar must be obtained from your Oracle distribution. Gerrit
+initialization process tries to copy it from a known location:
+
+----
+/u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
+----
+
+If this file can not be located at this place, then the alternative location
+can be provided.
+
+Instance name is the Oracle SID. Sample database section in
+$site_path/etc/gerrit.config:
+
+----
+[database]
+        type = oracle
+        instance = xe
+        hostname = localhost
+        username = gerrit2
+        port = 1521
+----
+
+Sample database section in $site_path/etc/secure.config:
+
+----
+[database]
+        password = secret_pasword
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
new file mode 100644
index 0000000..15c145a
--- /dev/null
+++ b/Documentation/dev-buck.txt
@@ -0,0 +1,376 @@
+Gerrit Code Review - Building with Buck
+=======================================
+
+
+Installation
+------------
+
+There is currently no binary distribution of Buck, so it has to be manually
+built and installed.  Apache Ant is required.  Currently only Linux and Mac
+OS are supported.
+
+Clone the git and build it:
+
+----
+  git clone https://gerrit.googlesource.com/buck
+  cd buck
+  ant
+----
+
+Make sure you have a `bin/` directory in your home directory and that
+it is included in your path:
+
+----
+  mkdir ~/bin
+  PATH=~/bin:$PATH
+----
+
+Add a symbolic link in `~/bin` to the buck executable:
+
+----
+  ln -s `pwd`/bin/buck ~/bin/
+----
+
+Verify that `buck` is accessible:
+
+----
+  which buck
+----
+
+If you plan to use the link:#buck-daemon[Buck daemon] add a symbolic
+link in `~/bin` to the buckd executable:
+
+----
+  ln -s `pwd`/bin/buckd ~/bin/
+----
+
+To enable autocompletion of buck commands, install the autocompletion
+script from `./scripts/bash_completion` in the buck project.  Refer to
+the script's header comments for installation instructions.
+
+
+[[eclipse]]
+Eclipse Integration
+-------------------
+
+
+Generating the Eclipse Project
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create the Eclipse project:
+
+----
+  tools/eclipse/project.py
+----
+
+In Eclipse, choose 'Import existing project' and select the `gerrit` project
+from the current working directory.
+
+Expand the `gerrit` project, right-click on the `buck-out` folder, select
+'Properties', and then under 'Attributes' check 'Derived'.
+
+Note that if you make any changes in the project configuration
+that get saved to the `.project` file, for example adding Resource
+Filters on a folder, they will be overwritten the next time you run
+`tools/eclipse/project.py`.
+
+
+Refreshing the Classpath
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+If an updated classpath is needed, the Eclipse project can be
+refreshed and missing dependency JARs can be downloaded:
+
+----
+  tools/eclipse/project.py
+----
+
+
+Attaching Sources
+~~~~~~~~~~~~~~~~~
+
+To save time and bandwidth source JARs are only downloaded by the buck
+build where necessary to compile Java source into JavaScript using the
+GWT compiler.  Additional sources may be obtained, allowing Eclipse to
+show documentation or dive into the implementation of a library JAR:
+
+----
+  tools/eclipse/project.py --src
+----
+
+
+[[build]]
+Building on the Command Line
+----------------------------
+
+
+Gerrit Development WAR File
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To build the Gerrit web application:
+
+----
+  buck build gerrit
+----
+
+The output executable WAR will be placed in:
+
+----
+  buck-out/gen/gerrit.war
+----
+
+
+Extension and Plugin API JAR Files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To build the extension and plugin API JAR files:
+
+----
+  buck build api
+----
+
+The output JAR files will be placed in:
+
+----
+  buck-out/gen/{extension,plugin}-api.jar
+----
+
+Install {extension,plugin}-api to the local maven repository:
+
+----
+  buck build api_install
+----
+
+Deploy {extension,plugin}-api to the remote maven repository
+
+----
+  buck build api_deploy
+----
+
+The type of the repo is induced from the Gerrit version name, i.e.
+* 2.8-SNAPSHOT: snapshot repo
+* 2.8: release repo
+
+Plugins
+~~~~~~~
+
+To build all core plugins:
+
+----
+  buck build plugins:core
+----
+
+The output JAR files for individual plugins will be placed in:
+
+----
+  buck-out/gen/plugins/<name>/<name>.jar
+----
+
+The JAR files will also be packaged in:
+
+----
+  buck-out/gen/plugins/core.zip
+----
+
+To build a specific plugin:
+
+----
+  buck build plugins/<name>
+----
+
+The output JAR file will be be placed in:
+
+----
+  buck-out/gen/plugins/<name>/<name>.jar
+----
+
+Note that when building an individual plugin, the `core.zip` package
+is not regenerated.
+
+
+[[documentation]]
+Documentation
+~~~~~~~~~~~~~
+
+To build the documentation:
+
+----
+  buck build docs
+----
+
+The generated html files will be placed in:
+
+----
+  buck-out/gen/Documentation/html__tmp/Documentation
+----
+
+The html files will also be bundled into `html.zip` in this location:
+
+----
+  buck-out/gen/Documentation/html.zip
+----
+
+[[release]]
+Gerrit Release WAR File
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To build the release of the Gerrit web application, including documentation and
+all core plugins:
+
+----
+  buck build release
+----
+
+The output release WAR will be placed in:
+
+----
+  buck-out/gen/release.war
+----
+
+[[tests]]
+Running Unit Tests
+------------------
+
+To run all tests including acceptance tests:
+
+----
+  buck test --all
+----
+
+To exclude slow tests:
+
+----
+  buck test --all --exclude slow
+----
+
+To run a specific test, e.g. the acceptance test
+`com.google.gerrit.acceptance.git.HttpPushForReviewIT`:
+
+----
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:HttpPushForReviewIT
+----
+
+
+Dependencies
+------------
+
+Dependency JARs are normally downloaded automatically, but Buck can inspect
+its graph and download any missing JAR files.  This is useful to enable
+subsequent builds to run without network access:
+
+----
+  tools/download_all.py
+----
+
+When downloading from behind a proxy (which is common in some corporate
+environments), it might be necessary to explicitly specify the proxy that
+is then used by `curl`:
+
+----
+  export http_proxy=http://<proxy_user_id>:<proxy_password>@<proxy_server>:<proxy_port>
+----
+
+Redirection to local mirrors of Maven Central and the Gerrit storage
+bucket is supported by defining specific properties in
+`local.properties`, a file that is not tracked by Git:
+
+----
+  echo download.GERRIT = http://nexus.my-company.com/ >>local.properties
+  echo download.MAVEN_CENTRAL = http://nexus.my-company.com/ >>local.properties
+----
+
+The `local.properties` file may be placed in the root of the gerrit repository
+being built, or in `~/.gerritcodereview/`.  The file in the root of the gerrit
+repository has precedence.
+
+Building against unpublished Maven JARs
+---------------------------------------
+
+To build against unpublished Maven JARs, like gwtorm or PrologCafe, the custom
+JARs must be installed in the local Maven repository (`mvn clean install`) and
+`maven_jar()` must be updated to point to the `MAVEN_LOCAL` Maven repository for
+that artifact:
+
+[source,python]
+----
+ maven_jar(
+   name = 'gwtorm',
+   id = 'gwtorm:gwtorm:42',
+   license = 'Apache2.0',
+   repository = MAVEN_LOCAL,
+ )
+----
+
+Caching Build Results
+~~~~~~~~~~~~~~~~~~~~~
+
+Build results can be locally cached, saving rebuild time when
+switching between Git branches. Buck's documentation covers
+caching in link:http://facebook.github.io/buck/concept/buckconfig.html[buckconfig].
+The trivial case using a local directory is:
+
+----
+  cat >.buckconfig.local <<EOF
+  [cache]
+    mode = dir
+    dir = buck-cache
+  EOF
+----
+
+[[buck-daemon]]
+Using Buck daemon
+~~~~~~~~~~~~~~~~~
+
+Buck ships with a daemon command `buckd`, which uses the
+link:https://github.com/martylamb/nailgun[Nailgun] protocol for running
+Java programs from the command line without incurring the JVM startup
+overhead.
+
+Using a Buck daemon can save significant amounts of time as it avoids the
+overhead of starting a Java virtual machine, loading the buck class files
+and parsing the build files for each command.
+
+It is safe to run several buck daemons started from different project
+directories and they will not interfere with each other. Buck's documentation
+covers daemon in http://facebook.github.io/buck/command/buckd.html[buckd].
+
+The trivial use case is to run `buckd` from the project's root directory and
+run `buck` as usual:
+
+----
+  buckd
+  buck build gerrit
+  Using buckd.
+  [-] PARSING BUILD FILES...FINISHED 0.6s
+  [-] BUILDING...FINISHED 0.2s
+----
+
+Override Buck's settings
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+User-specific configuration can be placed in one of the following files:
+`/etc/buck.conf`, `$HOME/.buck/buck.conf` or `$HOME/.buckrc`.
+
+For example to override Buck's default 1GB heap size:
+
+----
+  cat > $HOME/.buckrc <<EOF
+  export BUCK_EXTRA_JAVA_ARGS="\
+  -XX:MaxPermSize=512m \
+  -Xms8000m \
+  -Xmx16000m"
+  EOF
+----
+
+Or to debug BUCK, set `BUCK_DEBUG_MODE` to anything non-empty, then connect to
+port 8888:
+
+----
+  cat > $HOME/.buckrc <<EOF
+  export BUCK_DEBUG_MODE="yes"
+  EOF
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 84cb1e0..edc072d 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -94,13 +94,22 @@
 ====
 
 The Change-Id is, as usual, created by a local git hook.  To install it, simply
-copy one from the checkout and make it executable:
+copy it from the checkout and make it executable:
 
 ====
   cp ./gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg .git/hooks/
   chmod +x .git/hooks/commit-msg
 ====
 
+If you are working on core plugins, you will also need to install the
+same hook in the submodules:
+
+====
+  export hook=$(pwd)/.git/hooks/commit-msg
+  git submodule foreach 'cp -p "$hook" "$(git rev-parse --git-dir)/hooks/"'
+====
+
+
 To set up git's remote for easy pushing, run the following:
 
 ====
@@ -127,7 +136,7 @@
     in the formatting guidelines.  This is especially true within the
     same file.
   * Review your change in Gerrit to see if it highlights
-    mistakingly deleted/added spaces on lines, trailing spaces.
+    mistakenly deleted/added spaces on lines, trailing spaces.
   * Line length should be 80 or less, unless the code reads
     better with something slightly longer.  Shorter lines not only
     help reviewers who may use a tablet to review the code, but future
@@ -271,6 +280,10 @@
 Developers concerned with stable branches are encouraged to backport or push
 patchsets to these branches, even if no new release is planned.
 
+Fixes that are known to be needed for a particular release should be pushed
+for review on that release's stable branch.  It will then be included in
+the master branch when the stable branch is merged back.
+
 
 GERRIT
 ------
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 2e43b96..3cb58b1 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -66,7 +66,7 @@
 
 Gerrit 2.x is a complete rewrite of the Gerrit fork, completely
 changing the implementation from Python on Google App Engine, to Java
-on a J2EE servlet container and a SQL database.
+on a J2EE servlet container and an SQL database.
 
 * link:http://video.google.com/videoplay?docid=-8502904076440714866[Mondrian Code Review On The Web]
 * link:http://code.google.com/p/rietveld/[Rietveld - Code Review for Subversion]
@@ -445,7 +445,7 @@
 -------
 
 Gerrit targets for sub-250 ms per page request, mostly by using
-very compact JSON payloads bewteen client and server.  However, as
+very compact JSON payloads between client and server.  However, as
 most of the serving stack (network, hardware, metadata
 database) is out of control of the Gerrit developers, no real
 guarantees can be made about latency.
@@ -474,7 +474,7 @@
 Out of the box, Gerrit will handle the "Default Maximum". Site
 administrators may reconfigure their servers by editing gerrit.config
 to run closer to the estimated maximum if sufficient memory is made
-avaliable to the JVM and the relevant cache.*.memoryLimit variables
+available to the JVM and the relevant cache.*.memoryLimit variables
 are increased from their defaults.
 
 Discussion
@@ -671,7 +671,7 @@
 
 PostgreSQL and MySQL can be configured to replicate their data to
 other systems, where they are applied to a warm-standby backup in
-real time.  Gerrit instances which care about reduduncy will setup
+real time.  Gerrit instances which care about redundancy will setup
 this feature of PostgreSQL or MySQL to ensure the warm-standby is
 reasonably current should the master go offline.
 
@@ -699,7 +699,7 @@
 Changes submitted and merged into a branch also update the
 Git reflog.  These logs are available only to the Gerrit site
 administrator, and they are not replicated through the automatic
-replication noted earlier.  These logs are primarly recorded for an
+replication noted earlier.  These logs are primarily recorded for an
 "oh s**t" moment where the administrator has to rewind data.  In most
 installations they are a waste of disk space.  Future versions of
 JGit may allow disabling these logs, and Gerrit may take advantage
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 019c78f..d2fc8f0 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -8,19 +8,6 @@
 runtime debugging environment.
 
 
-[[maven]]
-Maven Plugin
-------------
-
-Install the Maven Integration plugins.
-
-In Eclipse version 3.7 (Indigo) and later, these are available in the
-default update site and can be found under the 'Collaboration' category.
-
-For older versions the update site must be manually added; the link can
-be found on the http://www.eclipse.org/m2e/download/[m2eclipse download page].
-
-
 [[Formatting]]
 Code Formatter Settings
 -----------------------
@@ -32,23 +19,12 @@
 settings prefer when formatting source code.
 
 
-Import Projects
----------------
-
-Import the projects into Eclipse by going to File -> Import... -> Maven ->
-Existing Maven Projects and selecting the directory containing pom.xml.
-
-Some of the source code is generated with ANTLR sources.  To build
-these files, right click on the imported projects, Maven -> Update
-Project Configuration.  This will resolve compile errors identified
-after import.
-
-
 Site Initialization
 -------------------
 
-link:dev-readme.html#build[Build] once on the command line and
-then follow link:dev-readme.html#init[Site Initialization] in the
+Build once on the command line with
+link:dev-buck.html#build[Buck] and then follow
+link:dev-readme.html#init[Site Initialization] in the
 Developer Setup guide to configure a local site for testing.
 
 
@@ -58,10 +34,10 @@
 Running the Daemon
 ~~~~~~~~~~~~~~~~~~
 
-Duplicate the existing `pgm_daemon` launch configuration:
+Duplicate the existing launch configuration:
 
 * Run -> Debug Configurations ...
-* Java Application -> `pgm_daemon`
+* Java Application -> `buck_daemon_ui_*`
 * Right click, Duplicate
 
 * Modify the name to be unique.
@@ -73,25 +49,16 @@
 
 * Switch to Common tab.
 * Change Save as to be Local file.
+* Close the Debug Configurations dialog and save the changes when prompted.
 
 
-[[hosted-mode]]
 Running Hosted Mode
 ~~~~~~~~~~~~~~~~~~~
 
-To debug the GWT code executing in the web browser, two additional Git
-repositories need to be cloned.
-
-* https://gerrit.googlesource.com/gwtjsonrpc
-* https://gerrit.googlesource.com/gwtorm
-
-In Eclipse, import the pom.xml file in the root directory of each of
-these cloned gits via General -> Maven Projects.
-
-Duplicate the existing `gwtui_dbg` launch configuration:
+Duplicate the existing launch configuration:
 
 * Run -> Debug Configurations ...
-* Java Application -> `gwtui_dbg`
+* Java Application -> `buck_gwt_debug`
 * Right click, Duplicate
 
 * Modify the name to be unique.
@@ -103,23 +70,22 @@
 
 * Switch to Common tab.
 * Change Save as to be Local file.
+* Close the Debug Configurations dialog and save the changes when prompted.
 
 
 [[known-problems]]
 Known problems
 --------------
 
-* When running Gerrit under the Eclipse debugger, code that attempts
-to load Prolog code may erroneously raise ClassNotFoundException,
-claiming that classes in the `Gerrit` package can't be found. The
-error can often be resolved by rebuilding Gerrit with `mvn package`
-and restarting the debug session.
-
 * OpenID authentication won't work in hosted mode, so you need to change
 the link:config-gerrit.html#auth.type[auth.type] configuration parameter
 to `DEVELOPMENT_BECOME_ANY_ACCOUNT` to disable OpenID and allow you to
 impersonate whatever account you otherwise would've used.
 
+* Error "Cannot create ReviewDb" occurs if the test site is already running.
+Stop the test site with `gerrit.sh stop` before attempting to run hosted mode
+debugging.
+
 * Gerrit site doesn't appear, only directory listing is shown. Web toolkit
 developer browser plugin is missing. If there is no warning, that browser
 plugin is missing with the suggestion to install it, you can install the
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 12838fe..717547b 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -38,7 +38,7 @@
 ----
 mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
     -DarchetypeArtifactId=gerrit-plugin-archetype \
-    -DarchetypeVersion=2.5-SNAPSHOT \
+    -DarchetypeVersion=2.8-SNAPSHOT \
     -DgroupId=com.google.gerrit \
     -DartifactId=testPlugin
 ----
@@ -49,14 +49,13 @@
 ask again for all properties including those with predefined default
 values.
 
-. clone the sample helloworld plugin:
+. clone the sample plugin:
 +
-This is a Maven project that adds an SSH command to Gerrit to print
-out a hello world message. It can be taken as an example to develop
-an own plugin.
+This is a project that demonstrates the various features of the
+plugin API. It can be taken as an example to develop an own plugin.
 +
 ----
-$ git clone https://gerrit.googlesource.com/plugins/helloworld
+$ git clone https://gerrit.googlesource.com/plugins/cookbook-plugin
 ----
 +
 When starting from this example one should take care to adapt the
@@ -64,7 +63,7 @@
 the plugin is developed. If the plugin is developed for a released
 Gerrit version (no `SNAPSHOT` version) then the URL for the
 `gerrit-api-repository` in the `pom.xml` needs to be changed to
-`https://gerrit-api.commondatastorage.googleapis.com/release/`.
+`https://gerrit-api.storage.googleapis.com/release/`.
 
 [[API]]
 API
@@ -133,6 +132,65 @@
   Gerrit-HttpModule: tld.example.project.HttpModuleClassName
 ====
 
+[[plugin_name]]
+Plugin Name
+~~~~~~~~~~~
+
+A plugin can optionally provide its own plugin name.
+
+====
+  Gerrit-PluginName: replication
+====
+
+This is useful for plugins that contribute plugin-owned capabilities that
+are stored in the `project.config` file. Another use case is to be able to put
+project specific plugin configuration section in `project.config`. In this
+case it is advantageous to reserve the plugin name to access the configuration
+section in the `project.config` file.
+
+If `Gerrit-PluginName` is omitted, then the plugin's name is determined from
+the plugin file name.
+
+If a plugin provides its own name, then that plugin cannot be deployed
+multiple times under different file names on one Gerrit site.
+
+For Maven driven plugins, the following line must be included in the pom.xml
+file:
+
+[source,xml]
+----
+<manifestEntries>
+  <Gerrit-PluginName>name</Gerrit-PluginName>
+</manifestEntries>
+----
+
+For Buck driven plugins, the following line must be included in the BUCK
+configuration file:
+
+[source,python]
+----
+manifest_entries = [
+   'Gerrit-PluginName: name',
+]
+----
+
+A plugin can get its own name injected at runtime:
+
+[source,java]
+----
+public class MyClass {
+
+  private final String pluginName;
+
+  @Inject
+  public MyClass(@PluginName String pluginName) {
+    this.pluginName = pluginName;
+  }
+
+  [...]
+}
+----
+
 [[reload_method]]
 Reload Method
 ~~~~~~~~~~~~~
@@ -193,7 +251,7 @@
 ====
 
 MyInitStep needs to follow the standard Gerrit InitStep syntax
-and behaviour: writing to the console using the injected ConsoleUI
+and behavior: writing to the console using the injected ConsoleUI
 and accessing / changing configuration settings using Section.Factory.
 
 In addition to the standard Gerrit init injections, plugins receive
@@ -210,13 +268,15 @@
 call the constructor of their connection class, passing in values obtained
 from the Section.Factory rather than from an injected Config object.
 
-Plugins InitStep are executing during the "Gerrit Plugin init" phase, after
-the extraction of the plugins embedded in Gerrit.war into $GERRIT_SITE/plugins
-and before the DB Schema initialization or upgrade.
-Plugins InitStep cannot refer to Gerrit DB Schema or any other Gerrit runtime
-objects injected at startup.
+Plugins' InitSteps are executed during the "Gerrit Plugin init" phase, after
+the extraction of the plugins embedded in the distribution .war file into
+`$GERRIT_SITE/plugins` and before the DB Schema initialization or upgrade.
 
-====
+A plugin's InitStep cannot refer to Gerrit's DB Schema or any other Gerrit
+runtime objects injected at startup.
+
+[source,java]
+----
 public class MyInitStep implements InitStep {
   private final ConsoleUI ui;
   private final Section.Factory sections;
@@ -237,7 +297,7 @@
     mySection.string("Link name", "linkname", "MyLink");
   }
 }
-====
+----
 
 [[classpath]]
 Classpath
@@ -255,6 +315,43 @@
 to package additional dependencies. Relocating (or renaming) classes
 should not be necessary due to the ClassLoader isolation.
 
+[[events]]
+Listening to Events
+-------------------
+
+Certain operations in Gerrit trigger events. Plugins may receive
+notifications of these events by implementing the corresponding
+listeners.
+
+* `com.google.gerrit.common.ChangeListener`:
++
+Allows to listen to change events. These are the same
+link:cmd-stream-events.html#events[events] that are also streamed by
+the link:cmd-stream-events.html[gerrit stream-events] command.
+
+* `com.google.gerrit.extensions.events.LifecycleListener`:
++
+Gerrit server startup and shutdown
+
+* `com.google.gerrit.extensions.events.NewProjectCreatedListener`:
++
+Project creation
+
+* `com.google.gerrit.extensions.events.ProjectDeletedListener`:
++
+Project deletion
+
+[[stream-events]]
+Sending Events to the Events Stream
+-----------------------------------
+
+Plugins may send events to the events stream where consumers of
+Gerrit's `stream-events` ssh command will receive them.
+
+To send an event, the plugin must invoke one of the `postEvent`
+methods in the `ChangeHookRunner` class, passing an instance of
+its own custom event class derived from `ChangeEvent`.
+
 [[ssh]]
 SSH Commands
 ------------
@@ -264,44 +361,47 @@
 
 Command implementations must extend the base class SshCommand:
 
-====
-  import com.google.gerrit.sshd.SshCommand;
+[source,java]
+----
+import com.google.gerrit.sshd.SshCommand;
 
-  class PrintHello extends SshCommand {
-    protected abstract void run() {
-      stdout.print("Hello\n");
-    }
+class PrintHello extends SshCommand {
+  protected abstract void run() {
+    stdout.print("Hello\n");
   }
-====
+}
+----
 
 If no Guice modules are declared in the manifest, SSH commands may
 use auto-registration by providing an `@Export` annotation:
 
-====
-  import com.google.gerrit.extensions.annotations.Export;
-  import com.google.gerrit.sshd.SshCommand;
+[source,java]
+----
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.sshd.SshCommand;
 
-  @Export("print")
-  class PrintHello extends SshCommand {
-    protected abstract void run() {
-      stdout.print("Hello\n");
-    }
+@Export("print")
+class PrintHello extends SshCommand {
+  protected abstract void run() {
+    stdout.print("Hello\n");
   }
-====
+}
+----
 
 If explicit registration is being used, a Guice module must be
 supplied to register the SSH command and declared in the manifest
 with the `Gerrit-SshModule` attribute:
 
-====
-  import com.google.gerrit.sshd.PluginCommandModule;
+[source,java]
+----
+import com.google.gerrit.sshd.PluginCommandModule;
 
-  class MyCommands extends PluginCommandModule {
-    protected void configureCommands() {
-      command("print").to(PrintHello.class);
-    }
+class MyCommands extends PluginCommandModule {
+  protected void configureCommands() {
+    command("print").to(PrintHello.class);
   }
-====
+}
+----
 
 For a plugin installed as name `helloworld`, the command implemented
 by PrintHello class will be available to users as:
@@ -310,6 +410,584 @@
 $ ssh -p 29418 review.example.com helloworld print
 ----
 
+Multiple SSH commands can be bound to the same implementation class. For
+example a Gerrit Shell plugin can bind different shell commands to the same
+implementation class:
+
+[source,java]
+----
+public class SshShellModule extends PluginCommandModule {
+  @Override
+  protected void configureCommands() {
+    command("ls").to(ShellCommand.class);
+    command("ps").to(ShellCommand.class);
+    [...]
+  }
+}
+----
+
+With the possible implementation:
+
+[source,java]
+----
+public class ShellCommand extends SshCommand {
+  @Override
+  protected void run() throws UnloggedFailure {
+    String cmd = getName().substring(getPluginName().length() + 1);
+    ProcessBuilder proc = new ProcessBuilder(cmd);
+    Process cmd = proc.start();
+    [...]
+  }
+}
+----
+
+And the call:
+
+----
+$ ssh -p 29418 review.example.com shell ls
+$ ssh -p 29418 review.example.com shell ps
+----
+
+[[configuration]]
+Configuration
+-------------
+
+In Gerrit, global configuration is stored in the `gerrit.config` file.
+If a plugin needs global configuration, this configuration should be
+stored in a `plugin` subsection in the `gerrit.config` file.
+
+This approach of storing the plugin configuration is only suitable for
+plugins that have a simple configuration that only consists of
+key-value pairs. With this approach it is not possible to have
+subsections in the plugin configuration. Plugins that require a complex
+configuration need to store their configuration in their own
+configuration file where they can make use of subsections. On the other
+hand storing the plugin configuration in a 'plugin' subsection in the
+`gerrit.config` file has the advantage that administrators have all
+configuration parameters in one file, instead of having one
+configuration file per plugin.
+
+To avoid conflicts with other plugins, it is recommended that plugins
+only use the `plugin` subsection with their own name. For example the
+`helloworld` plugin should store its configuration in the
+`plugin.helloworld` subsection:
+
+----
+[plugin "helloworld"]
+  language = Latin
+----
+
+Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
+plugin can easily access its configuration and there is no need for a
+plugin to parse the `gerrit.config` file on its own:
+
+[source,java]
+----
+@Inject
+private com.google.gerrit.server.config.PluginConfigFactory cfg;
+
+[...]
+
+String language = cfg.getFromGerritConfig("helloworld")
+                     .getString("language", "English");
+----
+
+[[project-specific-configuration]]
+Project Specific Configuration
+------------------------------
+
+In Gerrit, project specific configuration is stored in the project's
+`project.config` file on the `refs/meta/config` branch.  If a plugin
+needs configuration on project level (e.g. to enable its functionality
+only for certain projects), this configuration should be stored in a
+`plugin` subsection in the project's `project.config` file.
+
+This approach of storing the plugin configuration is only suitable for
+plugins that have a simple configuration that only consists of
+key-value pairs. With this approach it is not possible to have
+subsections in the plugin configuration. Plugins that require a complex
+configuration need to store their configuration in their own
+configuration file where they can make use of subsections. On the other
+hand storing the plugin configuration in a 'plugin' subsection in the
+`project.config` file has the advantage that project owners have all
+configuration parameters in one file, instead of having one
+configuration file per plugin.
+
+To avoid conflicts with other plugins, it is recommended that plugins
+only use the `plugin` subsection with their own name. For example the
+`helloworld` plugin should store its configuration in the
+`plugin.helloworld` subsection:
+
+----
+  [plugin "helloworld"]
+    enabled = true
+----
+
+Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
+plugin can easily access its project specific configuration and there
+is no need for a plugin to parse the `project.config` file on its own:
+
+[source,java]
+----
+@Inject
+private com.google.gerrit.server.config.PluginConfigFactory cfg;
+
+[...]
+
+boolean enabled = cfg.getFromProjectConfig(project, "helloworld")
+                     .getBoolean("enabled", false);
+----
+
+It is also possible to get missing configuration parameters inherited
+from the parent projects:
+
+[source,java]
+----
+@Inject
+private com.google.gerrit.server.config.PluginConfigFactory cfg;
+
+[...]
+
+boolean enabled = cfg.getFromProjectConfigWithInheritance(project, "helloworld")
+                     .getBoolean("enabled", false);
+----
+
+Project owners can edit the project configuration by fetching the
+`refs/meta/config` branch, editing the `project.config` file and
+pushing the commit back.
+
+[[capabilities]]
+Plugin Owned Capabilities
+-------------------------
+
+Plugins may provide their own capabilities and restrict usage of SSH
+commands to the users who are granted those capabilities.
+
+Plugins define the capabilities by overriding the `CapabilityDefinition`
+abstract class:
+
+[source,java]
+----
+public class PrintHelloCapability extends CapabilityDefinition {
+  @Override
+  public String getDescription() {
+    return "Print Hello";
+  }
+}
+----
+
+If no Guice modules are declared in the manifest, UI actions may
+use auto-registration by providing an `@Export` annotation:
+
+[source,java]
+----
+@Export("printHello")
+public class PrintHelloCapability extends CapabilityDefinition {
+  [...]
+}
+----
+
+Otherwise the capability must be bound in a plugin module:
+
+[source,java]
+----
+public class HelloWorldModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(CapabilityDefinition.class)
+      .annotatedWith(Exports.named("printHello"))
+      .to(PrintHelloCapability.class);
+  }
+}
+----
+
+With a plugin-owned capability defined in this way, it is possible to restrict
+usage of an SSH command or `UiAction` to members of the group that were granted
+this capability in the usual way, using the `RequiresCapability` annotation:
+
+[source,java]
+----
+@RequiresCapability("printHello")
+@CommandMetaData(name="print", description="Print greeting in different languages")
+public final class PrintHelloWorldCommand extends SshCommand {
+  [...]
+}
+----
+
+Or with `UiAction`:
+
+[source,java]
+----
+@RequiresCapability("printHello")
+public class SayHelloAction extends UiAction<RevisionResource>
+  implements RestModifyView<RevisionResource, SayHelloAction.Input> {
+  [...]
+}
+----
+
+Capability scope was introduced to differentiate between plugin-owned
+capabilities and core capabilities. Per default the scope of the
+`@RequiresCapability` annotation is `CapabilityScope.CONTEXT`, that means:
+
+* when `@RequiresCapability` is used within a plugin the scope of the
+capability is assumed to be that plugin.
+
+* If `@RequiresCapability` is used within the core Gerrit Code Review server
+(and thus is outside of a plugin) the scope is the core server and will use
+the `GlobalCapability` known to Gerrit Code Review server.
+
+If a plugin needs to use a core capability name (e.g. "administrateServer")
+this can be specified by setting `scope = CapabilityScope.CORE`:
+
+[source,java]
+----
+@RequiresCapability(value = "administrateServer", scope =
+    CapabilityScope.CORE)
+  [...]
+----
+
+[[ui_extension]]
+UI Extension
+------------
+
+Plugins can contribute UI actions on core Gerrit pages. This is useful
+for workflow customization or exposing plugin functionality through the
+UI in addition to SSH commands and the REST API.
+
+For instance a plugin to integrate Jira with Gerrit changes may
+contribute a "File bug" button to allow filing a bug from the change
+page or plugins to integrate continuous integration systems may
+contribute a "Schedule" button to allow a CI build to be scheduled
+manually from the patch set panel.
+
+Two different places on core Gerrit pages are supported:
+
+* Change screen
+* Project info screen
+
+Plugins contribute UI actions by implementing the `UiAction` interface:
+
+[source,java]
+----
+@RequiresCapability("printHello")
+class HelloWorldAction implements UiAction<RevisionResource>,
+    RestModifyView<RevisionResource, HelloWorldAction.Input> {
+  static class Input {
+    boolean french;
+    String message;
+  }
+
+  private Provider<CurrentUser> user;
+
+  @Inject
+  HelloWorldAction(Provider<CurrentUser> user) {
+    this.user = user;
+  }
+
+  @Override
+  public String apply(RevisionResource rev, Input input) {
+    final String greeting = input.french
+        ? "Bonjour"
+        : "Hello";
+    return String.format("%s %s from change %s, patch set %d!",
+        greeting,
+        Strings.isNullOrEmpty(input.message)
+            ? Objects.firstNonNull(user.get().getUserName(), "world")
+            : input.message,
+        rev.getChange().getId().toString(),
+        rev.getPatchSet().getPatchSetId());
+  }
+
+  @Override
+  public Description getDescription(
+      RevisionResource resource) {
+    return new Description()
+        .setLabel("Say hello")
+        .setTitle("Say hello in different languages");
+  }
+}
+----
+
+Sometimes plugins may want to be able to change the state of a patch set or
+change in the `UiAction.apply()` method and reflect these changes on the core
+UI. For example a buildbot plugin which exposes a 'Schedule' button on the
+patch set panel may want to disable that button after the build was scheduled
+and update the tooltip of that button. But because of Gerrit's caching
+strategy the following must be taken into consideration.
+
+The browser is allowed to cache the `UiAction` information until something on
+the change is modified. More accurately the change row needs to be modified in
+the database to have a more recent `lastUpdatedOn` or a new `rowVersion`, or
+the +refs/meta/config+ of the project or any parents needs to change to a new
+SHA-1. The ETag SHA-1 computation code can be found in the
+`ChangeResource.getETag()` method.
+
+The easiest way to accomplish this is to update `lastUpdatedOn` of the change:
+
+[source,java]
+----
+@Override
+public Object apply(RevisionResource rcrs, Input in) {
+  // schedule a build
+  [...]
+  // update change
+  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) {
+          ChangeUtil.updated(change);
+          return change;
+        }
+      });
+    db.commit();
+  } finally {
+    db.rollback();
+  }
+  [...]
+}
+----
+
+`UiAction` must be bound in a plugin module:
+
+[source,java]
+----
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    install(new RestApiModule() {
+      @Override
+      protected void configure() {
+        post(REVISION_KIND, "say-hello")
+            .to(HelloWorldAction.class);
+      }
+    });
+  }
+}
+----
+
+The module above must be declared in the `pom.xml` for Maven driven
+plugins:
+
+[source,xml]
+----
+<manifestEntries>
+  <Gerrit-Module>com.googlesource.gerrit.plugins.cookbook.Module</Gerrit-Module>
+</manifestEntries>
+----
+
+or in the `BUCK` configuration file for Buck driven plugins:
+
+[source,python]
+----
+manifest_entries = [
+  'Gerrit-Module: com.googlesource.gerrit.plugins.cookbook.Module',
+]
+----
+
+In some use cases more user input must be gathered, for that `UiAction` can be
+combined with the JavaScript API. This would display a small popup near the
+activation button to gather additional input from the user. The JS file is
+typically put in the `static` folder within the plugin's directory:
+
+[source,javascript]
+----
+Gerrit.install(function(self) {
+  function onSayHello(c) {
+    var f = c.textfield();
+    var t = c.checkbox();
+    var b = c.button('Say hello', {onclick: function(){
+      c.call(
+        {message: f.value, french: t.checked},
+        function(r) {
+          c.hide();
+          window.alert(r);
+          c.refresh();
+        });
+    }});
+    c.popup(c.div(
+      c.prependLabel('Greeting message', f),
+      c.br(),
+      c.label(t, 'french'),
+      c.br(),
+      b));
+    f.focus();
+  }
+  self.onAction('revision', 'say-hello', onSayHello);
+});
+----
+
+The JS module must be exposed as a `WebUiPlugin` and bound as
+an HTTP Module:
+
+[source,java]
+----
+public class HttpModule extends HttpPluginModule {
+  @Override
+  protected void configureServlets() {
+    DynamicSet.bind(binder(), WebUiPlugin.class)
+        .toInstance(new JavaScriptPlugin("hello.js"));
+  }
+}
+----
+
+The HTTP module above must be declared in the `pom.xml` for Maven
+driven plugins:
+
+[source,xml]
+----
+<manifestEntries>
+  <Gerrit-HttpModule>com.googlesource.gerrit.plugins.cookbook.HttpModule</Gerrit-HttpModule>
+</manifestEntries>
+----
+
+or in the `BUCK` configuration file for Buck driven plugins
+
+[source,python]
+----
+manifest_entries = [
+  'Gerrit-HttpModule: com.googlesource.gerrit.plugins.cookbook.HttpModule',
+]
+----
+
+If `UiAction` is annotated with the `@RequiresCapability` annotation, then the
+capability check is done during the `UiAction` gathering, so the plugin author
+doesn't have to set `UiAction.Description.setVisible()` explicitly in this
+case.
+
+The following prerequisities must be met, to satisfy the capability check:
+
+* user is authenticated
+* user is a member of a group which has the `Administrate Server` capability, or
+* user is a member of a group which has the required capability
+
+The `apply` method is called when the button is clicked. If `UiAction` is
+combined with JavaScript API (its own JavaScript function is provided),
+then a popup dialog is normally opened to gather additional user input.
+A new button is placed on the popup dialog to actually send the request.
+
+Every `UiAction` exposes a REST API endpoint. The endpoint from the example above
+can be accessed from any REST client, i. e.:
+
+====
+  curl -X POST -H "Content-Type: application/json" \
+    -d '{message: "François", french: true}' \
+    --digest --user joe:secret \
+    http://host:port/a/changes/1/revisions/1/cookbook~say-hello
+  "Bonjour François from change 1, patch set 1!"
+====
+
+A special case is to bind an endpoint without a view name.  This is
+particularly useful for `DELETE` requests:
+
+[source,java]
+----
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    install(new RestApiModule() {
+      @Override
+      protected void configure() {
+        delete(PROJECT_KIND)
+            .to(DeleteProject.class);
+      }
+    });
+  }
+}
+----
+
+For a `UiAction` bound this way, a JS API function can be provided.
+
+Currently only one restriction exists: per plugin only one `UiAction`
+can be bound per resource without view name. To define a JS function
+for the `UiAction`, "/" must be used as the name:
+
+[source,javascript]
+----
+Gerrit.install(function(self) {
+  function onDeleteProject(c) {
+    [...]
+  }
+  self.onAction('project', '/', onDeleteProject);
+});
+----
+
+[[top-menu-extensions]]
+Top Menu Extensions
+-------------------
+
+Plugins can contribute items to Gerrit's top menu.
+
+A single top menu extension can have multiple elements and will be put as
+the last element in Gerrit's top menu.
+
+Plugins define the top menu entries by implementing `TopMenu` interface:
+
+[source,java]
+----
+public class MyTopMenuExtension implements TopMenu {
+
+  @Override
+  public List<MenuEntry> getEntries() {
+    return Lists.newArrayList(
+               new MenuEntry("Top Menu Entry", Lists.newArrayList(
+                      new MenuItem("Gerrit", "http://gerrit.googlecode.com/"))));
+  }
+}
+----
+
+Plugins can also add additional menu items to Gerrit's top menu entries
+by defining a `MenuEntry` that has the same name as a Gerrit top menu
+entry:
+
+[source,java]
+----
+public class MyTopMenuExtension implements TopMenu {
+
+  @Override
+  public List<MenuEntry> getEntries() {
+    return Lists.newArrayList(
+               new MenuEntry(GerritTopMenu.PROJECTS, Lists.newArrayList(
+                      new MenuItem("Browse Repositories", "https://gerrit.googlesource.com/"))));
+  }
+}
+----
+
+If no Guice modules are declared in the manifest, the top menu extension may use
+auto-registration by providing an `@Listen` annotation:
+
+[source,java]
+----
+@Listen
+public class MyTopMenuExtension implements TopMenu {
+  [...]
+}
+----
+
+Otherwise the top menu extension must be bound in the plugin module used
+for the Gerrit system injector (Gerrit-Module entry in MANIFEST.MF):
+
+[source,java]
+----
+package com.googlesource.gerrit.plugins.helloworld;
+
+public class HelloWorldModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), TopMenu.class).to(MyTopMenuExtension.class);
+  }
+}
+----
+
+[source,manifest]
+----
+Gerrit-ApiType: plugin
+Gerrit-Module: com.googlesource.gerrit.plugins.helloworld.HelloWorldModule
+----
+
 [[http]]
 HTTP Servlets
 -------------
@@ -319,38 +997,40 @@
 
 Servlets may use auto-registration to declare the URL they handle:
 
-====
-  import com.google.gerrit.extensions.annotations.Export;
-  import com.google.inject.Singleton;
-  import javax.servlet.http.HttpServlet;
-  import javax.servlet.http.HttpServletRequest;
-  import javax.servlet.http.HttpServletResponse;
+[source,java]
+----
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Singleton;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
-  @Export("/print")
-  @Singleton
-  class HelloServlet extends HttpServlet {
-    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
-      res.setContentType("text/plain");
-      res.setCharacterEncoding("UTF-8");
-      res.getWriter().write("Hello");
-    }
+@Export("/print")
+@Singleton
+class HelloServlet extends HttpServlet {
+  protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+    res.setContentType("text/plain");
+    res.setCharacterEncoding("UTF-8");
+    res.getWriter().write("Hello");
   }
-====
+}
+----
 
 The auto registration only works for standard servlet mappings like
 `/foo` or `/foo/*`. Regex style bindings must use a Guice ServletModule
 to register the HTTP servlets and declare it explicitly in the manifest
 with the `Gerrit-HttpModule` attribute:
 
-====
-  import com.google.inject.servlet.ServletModule;
+[source,java]
+----
+import com.google.inject.servlet.ServletModule;
 
-  class MyWebUrls extends ServletModule {
-    protected void configureServlets() {
-      serve("/print").with(HelloServlet.class);
-    }
+class MyWebUrls extends ServletModule {
+  protected void configureServlets() {
+    serve("/print").with(HelloServlet.class);
   }
-====
+}
+----
 
 For a plugin installed as name `helloworld`, the servlet implemented
 by HelloServlet class will be available to users as:
@@ -369,12 +1049,27 @@
 
 Plugins can use this to store any data they want.
 
-====
-  @Inject
-  MyType(@PluginData java.io.File myDir) {
-    new FileInputStream(new File(myDir, "my.config"));
-  }
-====
+[source,java]
+----
+@Inject
+MyType(@PluginData java.io.File myDir) {
+  new FileInputStream(new File(myDir, "my.config"));
+}
+----
+
+[[download-commands]]
+Download Commands
+-----------------
+
+Gerrit offers commands for downloading changes using different
+download schemes (e.g. for downloading via different network
+protocols). Plugins can contribute download schemes and download
+commands by implementing
+`com.google.gerrit.extensions.config.DownloadScheme` and
+`com.google.gerrit.extensions.config.DownloadCommand`.
+
+The download schemes and download commands which are used most often
+are provided by the Gerrit core plugin `download-commands`.
 
 [[documentation]]
 Documentation
@@ -384,11 +1079,11 @@
 `/Documentation/*` or `/static/*`, the core Gerrit server will
 automatically export these resources over HTTP from the plugin JAR.
 
-Static resources under `static/` directory in the JAR will be
+Static resources under the `static/` directory in the JAR will be
 available as `/plugins/helloworld/static/resource`. This prefix is
 configurable by setting the `Gerrit-HttpStaticPrefix` attribute.
 
-Documentation files under `Documentation/` directory in the JAR
+Documentation files under the `Documentation/` directory in the JAR
 will be available as `/plugins/helloworld/Documentation/resource`. This
 prefix is configurable by setting the `Gerrit-HttpDocumentationPrefix`
 attribute.
@@ -444,9 +1139,21 @@
 of the file, minus the `*.html` extension, as the link text. Any
 hyphens in the file name will be replaced with spaces.
 
+If a discovered file is named `about.md` or `about.html`, its
+content will be inserted in an 'About' section at the top of the
+auto-generated index page.  If both `about.md` and `about.html`
+exist, only the first discovered file will be used.
+
 If a discovered file name beings with `cmd-` it will be clustered
-into a 'Commands' section of the generated index page. All other
-files are clustered under a 'Documentation' section.
+into a 'Commands' section of the generated index page.
+
+If a discovered file name beings with `servlet-` it will be clustered
+into a 'Servlets' section of the generated index page.
+
+If a discovered file name beings with `rest-api-` it will be clustered
+into a 'REST APIs' section of the generated index page.
+
+All other files are clustered under a 'Documentation' section.
 
 Some optional information from the manifest is extracted and
 displayed as part of the index page, if present in the manifest:
@@ -481,6 +1188,12 @@
 Disabled plugins can be re-enabled using the
 link:cmd-plugin-enable.html[plugin enable] command.
 
+SEE ALSO
+--------
+
+* link:js-api.html[JavaScript API]
+* link:dev-rest-api.html[REST API Developers' Notes]
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 67b4aad..ced8648 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -1,12 +1,13 @@
 Gerrit Code Review - Developer Setup
 ====================================
 
-Apache Maven is needed to compile the code, and a SQL database
-to house the review metadata.  H2 is recommended for development
+Facebook Buck is needed to compile the code, and an SQL database to
+house the review metadata.  H2 is recommended for development
 databases, as it requires no external server process.
 
-Get the Source
---------------
+
+Getting the Source
+------------------
 
 Create a new client workspace:
 
@@ -19,46 +20,27 @@
 the core plugins, which are included as git submodules, are also
 cloned.
 
+
+Compiling
+---------
+
+For details on how to build the source code with Buck, refer to:
+link:dev-buck.html#build[Building on the command line with Buck].
+
+
 Configuring Eclipse
 -------------------
 
 To use the Eclipse IDE for development, please see
-link:dev-eclipse.html[Eclipse Setup] for more details on how to
-configure the workspace with the Maven build scripts.
+link:dev-eclipse.html[Eclipse Setup].
 
+For details on how to configure the Eclipse workspace with Buck,
+refer to: link:dev-buck.html#eclipse[Eclipse integration with Buck].
 
-[[build]]
-Building
---------
-
-From the command line:
-
-----
-  mvn clean package
-----
-
-By default the build will run tests and build the documentation.
-
-To build without tests:
-
-----
-  mvn clean package -DskipTests
-----
-
-To build without documentation:
-
-----
-  mvn clean package -Dgerrit.documentation.skip
-----
-
-Output executable WAR will be placed in:
-
-----
-  gerrit-war/target/gerrit-*.war
-----
 
 Mac OS X
-~~~~~~~~
+--------
+
 On Mac OS X ensure "Java For Mac OS X 10.5 Upate 4" (or later) has
 been installed, and that `JAVA_HOME` is set to
 "/System/Library/Frameworks/JavaVM.framework/Versions/1.6/Home".
@@ -75,7 +57,7 @@
 testing site for development use:
 
 ----
-  java -jar gerrit-war/target/gerrit-*.war init -d ../test_site
+  java -jar buck-out/gen/gerrit.war init -d ../test_site
 ----
 
 Accept defaults by pressing Enter until 'init' completes, or add
@@ -97,7 +79,8 @@
 Testing
 -------
 
-[[run-acceptance-tests]]
+
+[[tests]]
 Running the Acceptance Tests
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -108,15 +91,10 @@
 started on that site. When the test has finished the Gerrit daemon is
 shutdown.
 
-Since the acceptance tests are too expensive to run every time
-Gerrit is built, they are only executed during the Maven verify phase
-if the Maven profile `acceptance` is enabled.
+For instructions on running the integration tests with Buck,
+please refer to:
+link:dev-buck.html#tests[Running integration tests with Buck].
 
-To execute the acceptance tests run:
-
-----
-  mvn clean verify -Pacceptance
-----
 
 Running the Daemon
 ~~~~~~~~~~~~~~~~~~
@@ -125,7 +103,7 @@
 copying to the test site:
 
 ----
-  java -jar gerrit-war/target/gerrit-*.war daemon -d ../test_site
+  java -jar buck-out/gen/gerrit.war daemon -d ../test_site
 ----
 
 
@@ -136,7 +114,7 @@
 command line.  If the daemon is not currently running:
 
 ----
-  java -jar gerrit-war/target/gerrit-*.war gsql -d ../test_site
+  java -jar buck-out/gen/gerrit.war gsql -d ../test_site
 ----
 
 Or, if it is running and the database is in use, connect over SSH
@@ -213,11 +191,6 @@
 
 * http://code.google.com/webtoolkit/download.html[Download]
 
-Apache Maven:
-
-* http://maven.apache.org/download.html[Download]
-* http://maven.apache.org/run-maven/index.html[Running]
-
 Apache SSHD:
 
 * http://mina.apache.org/sshd/[SSHD]
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index ffdb6ea..9c98fff 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -73,7 +73,7 @@
     <repository>
       <id>gerrit-maven-repository</id>
       <name>Gerrit Maven Repository</name>
-      <url>s3://gerrit-maven@commondatastorage.googleapis.com</url>
+      <url>gs://gerrit-maven</url>
       <uniqueVersion>true</uniqueVersion>
     </repository>
   </distributionManagement>
@@ -86,9 +86,9 @@
   <build>
     <extensions>
       <extension>
-        <groupId>net.anzix.aws</groupId>
-        <artifactId>s3-maven-wagon</artifactId>
-        <version>3.2</version>
+        <groupId>com.googlesource.gerrit</groupId>
+        <artifactId>gs-maven-wagon</artifactId>
+        <version>3.3</version>
       </extension>
     </extensions>
   </build>
@@ -107,7 +107,7 @@
     <repository>
       <id>gerrit-plugins-repository</id>
       <name>Gerrit Plugins Repository</name>
-      <url>s3://gerrit-plugins@commondatastorage.googleapis.com</url>
+      <url>gs://gerrit-plugins</url>
       <uniqueVersion>true</uniqueVersion>
     </repository>
   </distributionManagement>
@@ -120,9 +120,9 @@
   <build>
     <extensions>
       <extension>
-        <groupId>net.anzix.aws</groupId>
-        <artifactId>s3-maven-wagon</artifactId>
-        <version>3.2</version>
+        <groupId>com.googlesource.gerrit</groupId>
+        <artifactId>gs-maven-wagon</artifactId>
+        <version>3.3</version>
       </extension>
     </extensions>
   </build>
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
index 5e3770d..956bd29 100644
--- a/Documentation/dev-release-subproject.txt
+++ b/Documentation/dev-release-subproject.txt
@@ -13,9 +13,9 @@
 `gerrit-api-repository` in the `pom.xml` is correct.
 +
 If `Gerrit-ApiVersion` references a released Gerrit version it must be
-`https://gerrit-api.commondatastorage.googleapis.com/release/`, if
+`https://gerrit-api.stoarge.googleapis.com/release/`, if
 `Gerrit-ApiVersion` references a snapshot Gerrit version it must be
-`https://gerrit-api.commondatastorage.googleapis.com/snapshot/`.
+`https://gerrit-api.storage.googleapis.com/snapshot/`.
 
 * Build the latest snapshot and install it into the local Maven
 repository:
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index cd7cd34..f9d0d0e 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -83,7 +83,7 @@
 
 
 Create the Actual Release
----------------------------
+-------------------------
 
 To create a Gerrit release the following steps have to be done:
 
@@ -157,7 +157,7 @@
 link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
 configuration needed for deployment]
 
-* Push the Jars to `commondatastorage.googleapis.com`:
+* Push the Jars to `storage.googleapis.com`:
 +
 ----
   ./tools/deploy_api.sh
diff --git a/Documentation/dev-rest-api.txt b/Documentation/dev-rest-api.txt
new file mode 100644
index 0000000..0175a62
--- /dev/null
+++ b/Documentation/dev-rest-api.txt
@@ -0,0 +1,89 @@
+Gerrit Code Review - REST API Developers' Notes
+===============================================
+
+This document is about developing the REST API.  For details of the
+actual APIs available in Gerrit, please see the
+link:rest-api.html[REST API interface reference].
+
+
+Testing REST API Functionality
+------------------------------
+
+
+Basic Testing
+~~~~~~~~~~~~~
+
+Basic testing of REST API functionality can be done with `curl`:
+
+----
+  curl http://localhost:8080/path/to/api/
+----
+
+By default, `curl` sends `GET` requests.  To test APIs with `PUT`, `POST`,
+or `DELETE`, an additional argument is required:
+
+----
+ curl -X PUT http://localhost:8080/path/to/api/
+ curl -X POST http://localhost:8080/path/to/api/
+ curl -X DELETE http://localhost:8080/path/to/api/
+----
+
+
+Sending Data in the Request
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Some REST APIs accept data in the request body of `PUT` and `POST` requests.
+
+Test data can be included from a local file:
+
+----
+  curl -X PUT -d@testdata.txt --header "Content-Type: application/json" http://localhost:8080/path/to/api/
+----
+
+Note that the `-d` option will remove the newlines from the content of the
+local file. If the content should be sent as-is then use the `--data-binary`
+option instead:
+
+----
+  curl -X PUT --data-binary @testdata.txt --header "Content-Type: text/plain" http://localhost:8080/path/to/api/
+----
+
+
+Authentication
+~~~~~~~~~~~~~~
+
+To test APIs that require authentication, the username and password must be specified on
+the command line:
+
+----
+ curl --digest --user username:password http://localhost:8080/a/path/to/api/
+----
+
+This makes it easy to switch users for testing of permissions.
+
+It is also possible to test with a username and password from the `.netrc`
+file (on Windows, `_netrc`):
+
+----
+ curl --digest -n http://localhost:8080/a/path/to/api/
+----
+
+In both cases, the password should be the user's link:user-upload.html#http[HTTP password].
+
+
+Verifying Header Content
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To verify the headers returned from a REST API call, use `curl` in verbose mode:
+
+----
+  curl -v -n --digest -X DELETE http://localhost:8080/a/path/to/api/
+----
+
+The headers on both the request and the response will be printed.
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
diff --git a/Documentation/doc.css b/Documentation/doc.css
new file mode 100644
index 0000000..a1c2333
--- /dev/null
+++ b/Documentation/doc.css
@@ -0,0 +1,37 @@
+body {
+  margin: 1em;
+}
+
+#toctitle {
+  margin-top: 0.5em;
+  font-weight: bold;
+}
+
+h1, h2, h3, h4, h5, h6, #toctitle {
+  color: #527bbd;
+  font-family: sans-serif;
+}
+
+h1, h2, h3 {
+  border-bottom: 2px solid silver;
+}
+
+p {
+  margin: 0.5em 0 0.5em 0;
+}
+li p {
+  margin: 0.2em 0 0.2em 0;
+}
+
+.listingblock > .content {
+  border: 2px solid silver;
+  background: #ebebeb;
+  margin-left: 2em;
+  width: 100em;
+  color: darkgreen;
+  padding: 2px;
+}
+
+dl dt {
+  margin-top: 1em;
+}
diff --git a/Documentation/error-has-duplicates.txt b/Documentation/error-has-duplicates.txt
index e9e42f4..b5175c0 100644
--- a/Documentation/error-has-duplicates.txt
+++ b/Documentation/error-has-duplicates.txt
@@ -1,5 +1,5 @@
-... has duplicates
-==================
+\... has duplicates
+===================
 
 With this error message Gerrit rejects to push a commit if its commit
 message contains a Change-ID for which multiple changes can be found
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt
index b13f3b4..edbc63b 100644
--- a/Documentation/error-missing-changeid.txt
+++ b/Documentation/error-missing-changeid.txt
@@ -6,11 +6,12 @@
 message if the commit message of the pushed commit does not contain
 a Change-Id in the footer (the last paragraph).
 
-This error may happen for two reasons:
+This error may happen for different reasons:
 
 . missing Change-Id in the commit message
 . Change-Id is contained in the commit message but not in the last
   paragraph
+. Change-Id is the only line in the commit message
 
 You can see the commit messages for existing commits in the history
 by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
@@ -51,6 +52,20 @@
 Change-ID into the last paragraph. How to update the commit message
 is explained link:error-push-fails-due-to-commit-message.html[here].
 
+Change-Id is the only line in the commit message
+------------------------------------------------
+
+Gerrit does not parse the subject of a commit message for the
+Change-Id even if this is the only and last paragraph of the commit
+message.
+
+If the Change-Id is the only line in the commit message you must update
+the commit message and insert a subject as the first line in the commit
+message. The Change-Id must be in the last paragraph of the commit
+message, i.e. separated from the subject by a blank line. How to update
+the commit message is explained
+link:error-push-fails-due-to-commit-message.html[here].
+
 
 GERRIT
 ------
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
new file mode 100755
index 0000000..fb03526
--- /dev/null
+++ b/Documentation/gen_licenses.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# 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.
+#
+# TODO(sop): Be more detailed: version, link to Maven Central
+
+from __future__ import print_function
+
+from collections import defaultdict, deque
+import re
+from shutil import copyfileobj
+from subprocess import Popen, PIPE
+from sys import stdout
+
+MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+
+def parse_graph():
+  graph = defaultdict(list)
+  p = Popen(
+    ['buck', 'audit', 'classpath', '--dot'] + MAIN,
+    stdout = PIPE)
+  for line in p.stdout:
+    m = re.search(r'"(//.*?)" -> "(//.*?)";', line)
+    if not m:
+      continue
+    target, dep = m.group(1), m.group(2)
+    if not target.endswith('__compile'):
+      graph[target].append(dep)
+  r = p.wait()
+  if r != 0:
+    exit(r)
+  return graph
+
+graph = parse_graph()
+licenses = defaultdict(set)
+
+queue = deque(MAIN)
+while queue:
+  target = queue.popleft()
+  for dep in graph[target]:
+    if not dep.startswith('//lib:LICENSE-'):
+      continue
+    licenses[dep].add(target)
+  queue.extend(graph[target])
+used = sorted(licenses.keys())
+
+print("""\
+Gerrit Code Review - Licenses
+=============================
+
+Gerrit open source software is licensed under the <<Apache2_0,Apache
+License 2.0>>.  Executable distributions also include other software
+components that are provided under additional licenses.
+
+[[cryptography]]
+Cryptography Notice
+-------------------
+
+This distribution includes cryptographic software.  The country
+in which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of encryption
+software.  BEFORE using any encryption software, please check
+your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software,
+to see if this is permitted.  See the
+link:http://www.wassenaar.org/[Wassenaar Arrangement]
+for more information.
+
+The U.S. Government Department of Commerce, Bureau of Industry
+and Security (BIS), has classified this software as Export
+Commodity Control Number (ECCN) 5D002.C.1, which includes
+information security software using or performing cryptographic
+functions with asymmetric algorithms.  The form and manner of
+this distribution makes it eligible for export under the License
+Exception ENC Technology Software Unrestricted (TSU) exception
+(see the BIS Export Administration Regulations, Section 740.13)
+for both object code and source code.
+
+Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
+uploads of changes directly from `git push` command line clients.
+
+Gerrit includes an SSH client (JSch), to support authenticated
+replication of changes to remote systems, such as for automatic
+updates of mirror servers, or realtime backups.
+
+For either feature to function, Gerrit requires the
+link:http://java.sun.com/javase/technologies/security/[Java Cryptography extensions]
+and/or the
+link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
+to be installed by the end-user.
+
+Licenses
+--------
+""")
+
+for n in used:
+  libs = sorted(licenses[n])
+  name = n[len('//lib:LICENSE-'):]
+  print()
+  print('[[%s]]' % name.replace('.', '_'))
+  print(name)
+  print('~' * len(name))
+  print()
+  for d in libs:
+    if d.startswith('//lib:') or d.startswith('//lib/'):
+      p = d[len('//lib:'):]
+    else:
+      p = d[d.index(':')+1:].lower()
+    print('* ' + p)
+  print()
+  print('----')
+  with open(n[2:].replace(':', '/')) as fd:
+    copyfileobj(fd, stdout)
+  print('----')
+
+print("""
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+""")
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 88b50fa..58dae6e 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -1,72 +1,87 @@
 Gerrit Code Review for Git
 ==========================
 
-Getting Started
----------------
+Index
+-----
 
-* link:intro-quick.html[A Quick Introduction To Gerrit]
+. General info
+.. link:licenses.html[Licenses and Notices]
+. Installing
+.. link:intro-quick.html[A Quick Introduction To Gerrit]
+.. link:intro-change-screen.html[A Quick Introduction To The New Change Screen]
+.. link:install.html[Installation Guide]
+. Tutorial
+.. Get started
+... External link: link:http://source.android.com/submit-patches/workflow[Default Android Workflow]
+.. Web
+... Registering a new Gerrit account
+... link:user-search.html[Searching Changes]
+... link:user-notify.html[Subscribing to Email Notifications]
+.. Ssh
+... ssh connection details
+... link:cmd-index.html[Command Line Tools]
+.. Git
+... git connection details
+... Commands, scenarios
+.... link:user-upload.html[Uploading Changes]
+.... link:error-messages.html[Error Messages]
+... Changes
+.... link:user-changeid.html[Change-Id Lines]
+.... link:user-signedoffby.html[Signed-off-by Lines]
+... Patch sets
+. Project management
+.. link:project-setup.html[Project Setup]
+.. link:access-control.html[Access Controls]
+... link:config-labels.html[Review Labels]
+... link:config-project-config.html[Access Controls Configuration Format]
+.. Multi-project management
+... Submodules
+... Repo
+.. Prolog rules
+... link:prolog-cookbook.html[Prolog Cookbook]
+... link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
+.. link:user-submodules.html[Subscribing to Git Submodules]
+.. Project sunset
+. Customization and integration
+.. link:user-dashboards.html[Dashboards]
+.. link:rest-api.html[REST API]
+.. link:config-gitweb.html[Gitweb Integration]
+.. link:config-themes.html[Themes]
+.. link:config-sso.html[Single Sign-On Systems]
+.. link:config-hooks.html[Hooks]
+.. link:config-mail.html[Mail Templates]
+.. link:config-cla.html[Contributor Agreements]
+. Server administration
+.. link:config-gerrit.html[System Settings]
+.. Backup
+.. Performance tuning
+... link:cmd-index.html[Command Line Tools]
+... Reading show-caches efficiently
+... How to read stats from the JVM
+.. High availability
+.. Replication
+.. link:https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F[Plugins]
+.. link:dev-design.html[System Design]
+.. link:config-contact.html[User Contact Information]
+.. link:config-reverseproxy.html[Reverse Proxy]
+.. link:config-auto-site-initialization.html[Automatic Site Initialization on Startup]
+.. link:pgm-index.html[Server Side Administrative Tools]
+. Developer
+.. link:dev-readme.html[Developer Setup]
+.. link:dev-buck.html[Building with Buck]
+.. link:dev-eclipse.html[Eclipse Setup]
+.. link:dev-contributing.html[Contributing to Gerrit]
+.. Documentation formatting guide for contributions
+.. link:dev-design.html[System Design]
+.. link:i18n-readme.html[i18n Support]
+.. Plugin development
+... link:dev-plugins.html[Developing Plugins]
+... link:js-api.html[JavaScript Plugin API]
+... link:config-validation.html[Commit Validation]
+. Maintainer
+.. link:dev-release.html[Developer Release]
+.. link:dev-release-subproject.html[Developer Subproject Release]
 
-End User Guide
---------------
-
-* External link: link:http://source.android.com/submit-patches/workflow[Default Android Workflow]
-* link:user-search.html[Searching Changes]
-* link:cmd-index.html[Command Line Tools]
-* link:user-upload.html[Uploading Changes]
-* link:user-changeid.html[Change-Id Lines]
-* link:user-signedoffby.html[Signed-off-by Lines]
-* link:error-messages.html[Error Messages]
-* link:user-notify.html[Subscribing to Email Notifications]
-
-Project Owner and Power User Guide
-----------------------------------
-
-* link:access-control.html[Access Controls]
-* link:rest-api.html[REST API]
-* link:user-dashboards.html[Dashboards]
-* link:user-submodules.html[Subscribing to Git Submodules]
-* link:prolog-cookbook.html[Prolog Cookbook]
-* link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
-* link:config-labels.html[Review Labels]
-
-Admin User Guide
-----------------
-
-* link:pgm-index.html[Server Side Administrative Tools]
-
-Installation
-~~~~~~~~~~~~
-
-* link:licenses.html[Licenses and Notices]
-* link:install.html[Installation Guide]
-* link:install-quick.html[Quick Installation in 10 Minutes]
-* link:project-setup.html[Project Setup]
-
-Configuration
-~~~~~~~~~~~~~
-
-* link:config-gerrit.html[System Settings]
-* link:config-contact.html[User Contact Information]
-* link:config-gitweb.html[Gitweb Integration]
-* link:config-themes.html[Themes]
-* link:config-sso.html[Single Sign-On Systems]
-* link:config-reverseproxy.html[Reverse Proxy]
-* link:config-hooks.html[Hooks]
-* link:config-mail.html[Mail Templates]
-* link:config-cla.html[Contributor Agreements]
-
-Gerrit Developer Documentation
-------------------------------
-
-* link:dev-readme.html[Developer Setup]
-* link:dev-eclipse.html[Eclipse Setup]
-* link:dev-contributing.html[Contributing to Gerrit]
-* link:dev-plugins.html[Developing Plugins]
-* link:config-validation.html[Commit Validation]
-* link:dev-design.html[System Design]
-* link:i18n-readme.html[i18n Support]
-* link:dev-release.html[Developer Release]
-* link:dev-release-subproject.html[Developer Subproject Release]
 
 Resources
 ---------
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 4927041..5ba8cb1 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -21,7 +21,7 @@
   link:install.html#init[site initialization] tasks described
   in the standard installation documentation.
 
-* Stop the embedded deamon that was automatically started by 'init':
+* Stop the embedded daemon that was automatically started by 'init':
 +
 ----
   review_site/bin/gerrit.sh stop
@@ -46,6 +46,9 @@
 from `'$site_path'/lib` into your servlet container's extensions
 directory so it's available to Gerrit Code Review.
 
+* ('Optional') link:config-auto-site-initialization.html[
+Configure Automatic Site Initialization on Startup]
+
 
 Jetty 7.x
 ---------
diff --git a/Documentation/install.txt b/Documentation/install.txt
index a18a506..1d6d1bd 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -9,7 +9,7 @@
 
 * JDK, minimum version 1.6 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
 
-You'll also need a SQL database to house the review metadata. You have the
+You'll also need an SQL database to house the review metadata. You have the
 choice of either using the embedded H2 or to host your own MySQL or PostgreSQL.
 
 
@@ -18,9 +18,8 @@
 ---------------
 
 Current and past binary releases of Gerrit can be obtained from
-the downloads page at the project site:
-
-* http://code.google.com/p/gerrit/downloads/list[Gerrit Downloads]
+the link:https://gerrit-releases.storage.googleapis.com/index.html[
+Gerrit Releases site].
 
 Download any current `*.war` package. The war will be referred to as
 `gerrit.war` from this point forward, so you may find it easier to
@@ -121,14 +120,29 @@
   review_site/bin/gerrit.sh restart
 ====
 
-('Optional') Link the gerrit.sh script into rc3.d so the daemon
-automatically starts and stops with the operating system:
+('Optional') Configure the daemon to automatically start and stop
+with the operating system.
+
+Uncomment the following 3 lines in the `'$site_path/bin/gerrit.sh'`
+script:
+
+====
+ chkconfig: 3 99 99
+ description: Gerrit Code Review
+ processname: gerrit
+====
+
+Then link the `gerrit.sh` script into `rc3.d`:
 
 ====
   sudo ln -snf `pwd`/review_site/bin/gerrit.sh /etc/init.d/gerrit
   sudo ln -snf /etc/init.d/gerrit /etc/rc3.d/S90gerrit
 ====
 
+('Optional') To enable autocompletion of the gerrit.sh commands, install
+autocompletion from the `/contrib/bash_completion` script.  Refer to the
+script's header comments for installation instructions.
+
 To install Gerrit into an existing servlet container instead of using
 the embedded Jetty server, see
 link:install-j2ee.html[J2EE installation].
@@ -146,6 +160,7 @@
 * link:config-themes.html[Themes]
 * link:config-gitweb.html[Gitweb Integration]
 * link:config-gerrit.html[Other System Settings]
+* link:config-auto-site-initialization.html[Automatic Site Initialization on Startup]
 
 
 [[anonymous_access]]
diff --git a/Documentation/intro-change-screen.txt b/Documentation/intro-change-screen.txt
new file mode 100644
index 0000000..2913336
--- /dev/null
+++ b/Documentation/intro-change-screen.txt
@@ -0,0 +1,216 @@
+Change Screen - Introduction
+============================
+
+As of Gerrit 2.8 the change screen was redesigned from the ground up. The old
+change screen is deprecated and will be discontinued in one of the next Gerrit
+releases.
+
+The design spirit of the new change screen is simplicity: only one patch set is
+presented on the screen. The list of related changes is always visible and
+optional elements are moved to pop down boxes.
+
+This is not only a facelift. The main highlights are under the hood:
+
+* Old style RPC calls are replaced by the REST API
+* The prettify syntax highlighting library was replaced by Codemirror
+* Automatic refresh of open changes
+* Support to download a patch direct in browser: no local repo is needed
+* JS API integration: it was never so easy to add change/revision actions to
+the UI from a plugin.
+
+This document is intended to help users to switch to the new change screen.
+
+Further information on the topic can be found in the:
+link:https://groups.google.com/forum/#!topic/repo-discuss/6Ryz9p6AzgE[
+CodeScreen2 thread on the repo-discuss mailing list].
+
+[[configuration]]
+Configuration
+-------------
+
+The new change screen is deactivated by default. It can be activated system-wide
+by changing the link:config-gerrit.html[gerrit.changeScreen] setting to
+`CHANGE_SCREEN2`.  Users can deactivate it by setting `OLD_UI` on their user
+preferences page.
+
+[[switching-between-patch-sets]]
+Switching between patch sets
+----------------------------
+
+As already mentioned above, the main difference between the old and the new
+change screen is the fact that only one patch set is presented on the screen.
+
+To switch to other patch sets for the given change, the drop down 'Revisions'
+box is used on the right upper side of the change header.
+
+Patch sets are always sorted in descending order. The option to switch between
+ascending and reverse patch set sorting order is not supported on the new change
+screen.
+
+[[download-commands]]
+Download commands
+-----------------
+
+The download commands are moved to the 'Download' drop down box.  Patch files
+can be downloaded as base64 encoded or zipped versions.
+
+[[quick-approve]]
+Quick approve
+-------------
+
+The so called 'Quick approve' button is some times confusing. Normal users (i.e.
+non-maintainers) see this as 'Verified+1' button to the right of the 'Reply'
+button.
+
+The button is not always "Verified+1". The button appears if a user has
+permission to vote the max score in exactly one label that the rules have marked
+as NEED.
+
+For a maintainer with both 'Verified+1' and 'Code-Review+2' powers the button
+does not appear, as both categories are still marked NEED and the maintainer has
+permission to use both.  If another maintainer scores 'Code-Review+2', then the
+button displays as 'Verified+1'. If a verifier scores 'Verified+1' the button
+displays as 'Code-Review+2'.
+
+It is important to note that by design, the user cannot provide a comment when
+using this button, hence the name 'Quick approve'. To provide comments, the
+'Reply' button should be used.
+
+[[reply-button]]
+Reply button
+------------
+
+This button corresponds to the 'Review' button the on patch set panel on the old
+change screen.  The only new feature: the user can optionaly send an email
+during the vote.
+
+Key bindings: "a" to open the drop down. "ESC" to close it.
+
+
+[[edit-commit-message]]
+Edit commit message
+-------------------
+
+To edit the commit message use the 'Edit Message' button on the change header,
+which will open a drop-down editor box.
+
+Key bindings: "e" to open the drop down. "ESC" to close it.
+
+[[edit-change-topic]]
+Edit change topic
+-----------------
+
+To edit the topic use the edit icon to the right of the topic field.
+
+Key bindings: "t" to open the drop down. "ESC" to close it.
+
+[[abandon-restore]]
+Abandon or Restore changes
+--------------------------
+
+When a change is abandoned or restored, a panel appears and a comment message
+can be provided.
+
+[[working-with-drafts]]
+Working with draft changes and patch sets
+-----------------------------------------
+
+When a change or a patch set is a draft, then three additional buttons appear on
+the action panel: 'Publish', 'Delete Revision', and 'Delete Change'. In the
+'Revisions' drop down a "(DRAFT)" suffix is added to the patch set number to
+indicate that the patch set is a draft.
+
+[[draft-comments]]
+Highlight draft comments
+------------------------
+
+If a patch set has draft comments that weren't published yet, then that patch
+set is marked on the list in the 'Revisions' drop down list. In addition a red
+"draft" prefix appears on the filenames in the file table.
+
+[[codemirror]]
+Codemirror
+----------
+
+On the user preferences page, 'Side By Side' or 'Unified Diff' view can be
+configured.  Use the "/" key to start the CodeMirror search, like in vim.
+
+Key bindings are not customizable at the moment. They may be added in the future.
+
+Range comments are supported on Codemirror's 'Side By Side' screen.  Highlight
+lines with the mouse and then click the bottom-most line number to create a
+range comment for the highlighted lines.
+
+[[reviewers]]
+Reviewers
+---------
+
+Reviewer are split into two groups: Reviewers who actually voted on the change
+in the 'Reviewers' field, and reviewers, who were added to the change but didn't
+vote yet in the 'CC' field.
+
+The votes per category are listed above the File list.
+
+To add a reviewer, use the '[+]' button to the right of the 'CC' field. Typing
+into the pop-up text field activates auto completion of user or group names.
+
+To remove reviewers click on the 'x' icon in the reviewer's "chip".
+
+Key bindings:  "c" to add a reviewer. "ESC" to close the drop down.
+
+[[auto-refresh]]
+Auto refresh of change data
+---------------------------
+
+On the new change screen polling for updates to the currently open change is
+activated per default.  For example, if another user votes or comments on the
+same change, then a popup window appears on the bottom right corner of the
+screen to notify the user that the change was updated.
+
+The default delay is 30 seconds.  It can be configured with the
+link:config-gerrit.html[change.updateDelay] setting.
+
+[[depends-on-needed-by]]
+"Depends on" and "Needed by"
+----------------------------
+
+Dependencies and dependent changes are listed in the 'Related Changes' drop
+down.
+
+Key bindings:  "J" & "K" to navigate between the related changes. "O" to
+open the currently selected related change.
+
+[[file-table]]
+File table
+----------
+
+The user can now manually toggle the 'reviewed' flag per file using the check
+box to the left of the filename.
+
+Key bindings: "j" & "k" to navigate in the file table, and "r" to toggle the
+'reviewed' flag.
+
+[[included-in]]
+Included in
+-----------
+
+To see the branches a specific change was merged into and the list of the tags
+a change was tagged with, use the 'Included In' drop down on the change header,
+to the left of the 'Revisions' drop down.
+
+Note that this list is only visible on merged changes.
+
+[[missing-features]]
+Missing features
+----------------
+
+Several features have not been implemented yet:
+
+* Permalink a change
+* Allow to see if a reviewer can't vote on a label
+* Allow to select a reference version as base for the comparison
+* Change diff view preferences
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
new file mode 100644
index 0000000..2e97ac4
--- /dev/null
+++ b/Documentation/js-api.txt
@@ -0,0 +1,638 @@
+Gerrit Code Review - JavaScript API
+===================================
+
+Gerrit Code Review supports an API for JavaScript plugins to interact
+with the web UI and the server process.
+
+Entry Point
+-----------
+
+JavaScript is loaded using a standard `<script src='...'>` HTML tag.
+Plugins should protect the global namespace by defining their code
+within an anonymous function passed to `Gerrit.install()`. The plugin
+will be passed an object describing its registration with Gerrit:
+
+[source,javascript]
+----
+Gerrit.install(function (self) {
+  // ... plugin JavaScript code here ...
+});
+----
+
+
+[[self]]
+Plugin Instance
+---------------
+
+The plugin instance is passed to the plugin's initialization function
+and provides a number of utility services to plugin authors.
+
+[[self_delete]]
+self.delete()
+~~~~~~~~~~~~~
+Issues a DELETE REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+Gerrit.delete(url, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* callback: JavaScript function to be invoked with the parsed
+  JSON result of the API call. DELETE methods often return
+  `204 No Content`, which is passed as null.
+
+[[self_get]]
+self.get()
+~~~~~~~~~~
+Issues a GET REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+self.get(url, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[[self_getPluginName]]
+self.getPluginName()
+~~~~~~~~~~~~~~~~~~~~
+Returns the name this plugin was installed as by the server
+administrator. The plugin name is required to access REST API
+views installed by the plugin, or to access resources.
+
+[[self_post]]
+self.post()
+~~~~~~~~~~~
+Issues a POST REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+self.post(url, input, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+self.post(
+  '/my-servlet',
+  {start_build: true, platform_type: 'Linux'},
+  function (r) {});
+----
+
+[[self_put]]
+self.put()
+~~~~~~~~~~
+Issues a PUT REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+self.put(url, input, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+  library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+self.put(
+  '/builds',
+  {start_build: true, platform_type: 'Linux'},
+  function (r) {});
+----
+
+[[self_onAction]]
+self.onAction()
+~~~~~~~~~~~~~~~
+Register a JavaScript callback to be invoked when the user clicks
+on a button associated with a server side `UiAction`.
+
+.Signature
+[source,javascript]
+----
+Gerrit.onAction(type, view_name, callback);
+----
+
+* type: `'change'`, `'revision'` or `'project'`, indicating which type
+  of resource the `UiAction` was bound to in the server.
+
+* view_name: string appearing in URLs to name the view. This is the
+  second argument of the `get()`, `post()`, `put()`, and `delete()`
+  binding methods in a `RestApiModule`.
+
+* callback: JavaScript function to invoke when the user clicks. The
+  function will be passed a link:#ActionContext[action context].
+
+[[self_url]]
+self.url()
+~~~~~~~~~~
+Returns a URL within the plugin's URL space. If invoked with no
+parameter the URL of the plugin is returned. If passed a string
+the argument is appended to the plugin URL.
+
+[source,javascript]
+----
+self.url();                    // "https://gerrit-review.googlesource.com/plugins/demo/"
+self.url('/static/icon.png');  // "https://gerrit-review.googlesource.com/plugins/demo/static/icon.png"
+----
+
+
+[[ActionContext]]
+Action Context
+--------------
+A new action context is passed to the `onAction` callback function
+each time the associated action button is clicked by the user. A
+context is initialized with sufficient state to issue the associated
+REST API RPC.
+
+[[context_action]]
+context.action
+~~~~~~~~~~~~~~
+An link:rest-api-changes.html#action-info[ActionInfo] object instance
+supplied by the server describing the UI button the user used to
+invoke the action.
+
+[[context_call]]
+context.call()
+~~~~~~~~~~~~~~
+Issues the REST API call associated with the action. The HTTP method
+used comes from `context.action.method`, hiding the JavaScript from
+needing to care.
+
+.Signature
+[source,javascript]
+----
+context.call(input, callback)
+----
+
+* input: JavaScript object to serialize as the request payload. This
+  parameter is ignored for GET and DELETE methods.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.call(
+  {message: "..."},
+  function (result) {
+    // ... use result here ...
+  });
+----
+
+[[context_change]]
+context.change
+~~~~~~~~~~~~~~
+When the action is invoked on a change a
+link:rest-api-changes.html#change-info[ChangeInfo] object instance
+describing the change. Available fields of the ChangeInfo may vary
+based on the options used by the UI when it loaded the change.
+
+[[context_delete]]
+context.delete()
+~~~~~~~~~~~~~~~~
+Issues a DELETE REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.delete(callback)
+----
+
+* callback: JavaScript function to be invoked with the parsed
+  JSON result of the API call. DELETE methods often return
+  `204 No Content`, which is passed as null.
+
+[source,javascript]
+----
+context.delete(function () {});
+----
+
+[[context_get]]
+context.get()
+~~~~~~~~~~~~~
+Issues a GET REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.get(callback)
+----
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.get(function (result) {
+  // ... use result here ...
+});
+----
+
+[[context_go]]
+context.go()
+~~~~~~~~~~~~
+Go to a page. Shorthand for link:#Gerrit_go[`Gerrit.go()`].
+
+[[context_hide]]
+context.hide()
+~~~~~~~~~~~~~~
+Hide the currently visible popup displayed by
+link:#context_popup[`context.popup()`].
+
+[[context_post]]
+context.post()
+~~~~~~~~~~~~~~
+Issues a POST REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.post(input, callback)
+----
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.post(
+  {message: "..."},
+  function (result) {
+    // ... use result here ...
+  });
+----
+
+[[context_popup]]
+context.popup()
+~~~~~~~~~~~~~~~
+
+Displays a small popup near the activation button to gather
+additional input from the user before executing the REST API RPC.
+
+The caller is always responsible for closing the popup with
+link#context_hide[`context.hide()`]. Gerrit will handle closing a
+popup if the user presses `Escape` while keyboard focus is within
+the popup.
+
+.Signature
+[source,javascript]
+----
+context.popup(element)
+----
+
+* element: an HTML DOM element to display as the body of the
+  popup. This is typically a `div` element but can be any valid HTML
+  element. CSS can be used to style the element beyond the defaults.
+
+A common usage is to gather more input:
+
+[source,javascript]
+----
+self.onAction('revision', 'start-build', function (c) {
+  var l = c.checkbox();
+  var m = c.checkbox();
+  c.popup(c.div(
+    c.div(c.label(l, 'Linux')),
+    c.div(c.label(m, 'Mac OS X')),
+    c.button('Build', {onclick: function() {
+      c.call(
+        {
+          commit: c.revision.name,
+          linux: l.checked,
+          mac: m.checked,
+        },
+        function() { c.hide() });
+    });
+});
+----
+
+[[context_put]]
+context.put()
+~~~~~~~~~~~~~
+Issues a PUT REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.put(input, callback)
+----
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.put(
+  {message: "..."},
+  function (result) {
+    // ... use result here ...
+  });
+----
+
+[[context_refresh]]
+context.refresh()
+~~~~~~~~~~~~~~~~~
+Refresh the current display. Shorthand for
+link:#Gerrit_refresh[`Gerrit.refresh()`].
+
+[[context_revision]]
+context.revision
+~~~~~~~~~~~~~~~~
+When the action is invoked on a specific revision of a change,
+a link:rest-api-changes.html#revision-info[RevisionInfo]
+object instance describing the revision. Available fields of the
+RevisionInfo may vary based on the options used by the UI when it
+loaded the change.
+
+[[context_project]]
+context.project
+~~~~~~~~~~~~~~~
+When the action is invoked on a specific project,
+the name of the project.
+
+Action Context HTML Helpers
+---------------------------
+The link:#ActionContext[action context] includes some HTML helper
+functions to make working with DOM based widgets less painful.
+
+* `br()`: new `<br>` element.
+
+* `button(label, options)`: new `<button>` with the string `label`
+  wrapped inside of a `div`. The optional `options` object may
+  define `onclick` as a function to be invoked upon clicking. This
+  calling pattern avoids circular references between the element
+  and the onclick handler.
+
+* `checkbox()`: new `<input type='checkbox'>` element.
+* `div(...)`: a new `<div>` wrapping the (optional) arguments.
+* `hr()`: new `<hr>` element.
+
+* `label(c, label)`: a new `<label>` element wrapping element `c`
+  and the string `label`. Used to wrap a checkbox with its label,
+  `label(checkbox(), 'Click Me')`.
+
+* `prependLabel(label, c)`: a new `<label>` element wrapping element `c`
+  and the string `label`. Used to wrap an input field with its label,
+  `prependLabel('Greeting message', textfield())`.
+
+* `textarea(options)`: new `<textarea>` element. The options
+  object may optionally include `rows` and `cols`. The textarea
+  comes with an onkeypress handler installed to play nicely with
+  Gerrit's keyboard binding system.
+
+* `textfield()`: new `<input type='text'>` element.  The text field
+  comes with an onkeypress handler installed to play nicely with
+  Gerrit's keyboard binding system.
+
+* `span(...)`: a new `<span>` wrapping the (optional) arguments.
+
+* `msg(label)`: a new label.
+
+[[Gerrit]]
+Gerrit
+------
+
+The `Gerrit` object is the only symbol provided into the global
+namespace by Gerrit Code Review. All top-level functions can be
+accessed through this name.
+
+[[Gerrit_delete]]
+Gerrit.delete()
+~~~~~~~~~~~~~~~
+Issues a DELETE REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_delete[self.delete()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.delete(url, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* callback: JavaScript function to be invoked with the parsed
+  JSON result of the API call. DELETE methods often return
+  `204 No Content`, which is passed as null.
+
+[source,javascript]
+----
+Gerrit.delete(
+  '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
+  function () {});
+----
+
+[[Gerrit_get]]
+Gerrit.get()
+~~~~~~~~~~~~
+Issues a GET REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_get[self.get()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.get(url, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+Gerrit.get('/changes/?q=status:open', function (open) {
+  for (var i = 0; i < open.length; i++) {
+    console.log(open.get(i).change_id);
+  }
+});
+----
+
+[[Gerrit_getPluginName]]
+Gerrit.getPluginName()
+~~~~~~~~~~~~~~~~~~~~~~
+Returns the name this plugin was installed as by the server
+administrator. The plugin name is required to access REST API
+views installed by the plugin, or to access resources.
+
+Unlike link:#self_getPluginName[`self.getPluginName()`] this method
+must guess the name from the JavaScript call stack. Plugins are
+encouraged to use `self.getPluginName()` whenever possible.
+
+[[Gerrit_go]]
+Gerrit.go()
+~~~~~~~~~~~
+Updates the web UI to display the view identified by the supplied
+URL token. The URL token is the text after `#` in the browser URL.
+
+[source,javascript]
+----
+Gerrit.go('/admin/projects/');
+----
+
+If the URL passed matches `http://...`, `https://...`, or `//...`
+the current browser window will navigate to the non-Gerrit URL.
+The user can return to Gerrit with the back button.
+
+[[Gerrit_install]]
+Gerrit.install()
+~~~~~~~~~~~~~~~~
+Registers a new plugin by invoking the supplied initialization
+function. The function is passed the link:#self[plugin instance].
+
+[source,javascript]
+----
+Gerrit.install(function (self) {
+  // ... plugin JavaScript code here ...
+});
+----
+
+[[Gerrit_post]]
+Gerrit.post()
+~~~~~~~~~~~~~
+Issues a POST REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_post[self.post()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.post(url, input, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+Gerrit.post(
+  '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
+  {topic: 'tests', message: 'Classify work as for testing.'},
+  function (r) {});
+----
+
+[[Gerrit_put]]
+Gerrit.put()
+~~~~~~~~~~~~
+Issues a PUT REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_put[self.put()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.put(url, input, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+  link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+  result of the API call. If the API returns a string the result is
+  a string, otherwise the result is a JavaScript object or array,
+  as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+Gerrit.put(
+  '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
+  {topic: 'tests', message: 'Classify work as for testing.'},
+  function (r) {});
+----
+
+[[Gerrit_onAction]]
+Gerrit.onAction()
+~~~~~~~~~~~~~~~~~
+Register a JavaScript callback to be invoked when the user clicks
+on a button associated with a server side `UiAction`.
+
+.Signature
+[source,javascript]
+----
+Gerrit.onAction(type, view_name, callback);
+----
+
+* type: `'change'` or `'revision'`, indicating what sort of resource
+  the `UiAction` was bound to in the server.
+
+* view_name: string appearing in URLs to name the view. This is the
+  second argument of the `get()`, `post()`, `put()`, and `delete()`
+  binding methods in a `RestApiModule`.
+
+* callback: JavaScript function to invoke when the user clicks. The
+  function will be passed a link:#ActionContext[ActionContext].
+
+[[Gerrit_refresh]]
+Gerrit.refresh()
+~~~~~~~~~~~~~~~~
+Redisplays the current web UI view, refreshing all information.
+
+[[Gerrit_url]]
+Gerrit.url()
+~~~~~~~~~~~~
+Returns the URL of the Gerrit Code Review server. If invoked with
+no parameter the URL of the site is returned. If passed a string
+the argument is appended to the site URL.
+
+[source,javascript]
+----
+Gerrit.url();        // "https://gerrit-review.googlesource.com/"
+Gerrit.url('/123');  // "https://gerrit-review.googlesource.com/123"
+----
+
+For a plugin specific version see link:#self_url()[`self.url()`].
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 3893e3f..2836e5b 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -115,11 +115,13 @@
 createdOn:: Time in seconds since the UNIX epoch when this patchset
 was created.
 
+isDraft:: Whether or not the patch set is a draft patch set.
+
 approvals:: The <<approval,approval attribute>> granted.
 
 comments:: All comments for this patchset in <<patchsetcomment,patchsetComment attributes>>.
 
-files:: All changed files in this patchset in <<patch,patch attributes>>.
+files:: All changed files in this patchset in <<file,file attributes>>.
 
 sizeInsertions:: Size information of insertions of this patchset.
 
@@ -235,9 +237,9 @@
 
 message:: The comment text.
 
-[[patch]]
-patch
------
+[[file]]
+file
+----
 Information about a patch on a file.
 
 file:: The name of the file.  If the file is renamed, the new name.
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
deleted file mode 100644
index fbdd9cd..0000000
--- a/Documentation/licenses.txt
+++ /dev/null
@@ -1,1189 +0,0 @@
-Gerrit Code Review - Licenses
-=============================
-
-Gerrit itself is licensed under the <<apache2,Apache License 2.0>>.
-Gerrit's executable distributions also include many other software
-components that are covered by additional licenses.
-
-Included Components
--------------------
-
-[options="header"]
-|======================================================================
-|Included Package           | License
-|Gerrit Code Review         | <<apache2,Apache License 2.0>>
-|gwtjsonrpc                 | <<apache2,Apache License 2.0>>
-|gwtorm                     | <<apache2,Apache License 2.0>>
-|Google Gson                | <<apache2,Apache License 2.0>>
-|Google Web Toolkit         | <<apache2,Apache License 2.0>>
-|Guice                      | <<apache2,Apache License 2.0>>
-|Guava Libraries            | <<apache2,Apache License 2.0>>
-|Apache Commons Codec       | <<apache2,Apache License 2.0>>
-|Apache Commons DBCP        | <<apache2,Apache License 2.0>>
-|Apache Commons Http Client | <<apache2,Apache License 2.0>>
-|Apache Commons Lang        | <<apache2,Apache License 2.0>>
-|Apache Commons Logging     | <<apache2,Apache License 2.0>>
-|Apache Commons Net         | <<apache2,Apache License 2.0>>
-|Apache Commons Pool        | <<apache2,Apache License 2.0>>
-|Apache Log4J               | <<apache2,Apache License 2.0>>
-|Apache MINA                | <<apache2,Apache License 2.0>>
-|Apache Tomcat Servlet API  | <<apache2,Apache License 2.0>>
-|Apache SSHD                | <<apache2,Apache License 2.0>>, see also <<sshd,NOTICE>>
-|Apache Velocity            | <<apache2,Apache License 2.0>>
-|Apache Xerces              | <<apache2,Apache License 2.0>>
-|OpenID4Java                | <<apache2,Apache License 2.0>>
-|Neko HTML                  | <<apache2,Apache License 2.0>>
-|mime-util                  | <<apache2,Apache License 2.0>>
-|Jetty                      | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
-|Prolog Cafe                | <<prolog_cafe,EPL or GPL>>
-|Google Code Prettify       | <<apache2,Apache License 2.0>>
-|JavaEWAH                   | <<apache2,Apache License 2.0>>
-|JGit                       | <<jgit,New-Style BSD>>
-|JSch                       | <<sshd,New-Style BSD>>
-|PostgreSQL JDBC Driver     | <<postgresql,New-Style BSD>>
-|H2 Database                | <<h2,EPL or modified MPL>>
-|ObjectWeb ASM              | <<asm,New-Style BSD>>
-|ANTLR                      | <<antlr,New-Style BSD>>
-|args4j                     | <<args4j,MIT License>>
-|SLF4J                      | <<slf4j,MIT License>>
-|Clippy                     | <<clippy,MIT License>>
-|juniversalchardet          | <<mpl1_1,MPL 1.1>>
-|AOP Alliance               | Public Domain
-|JSR 305                    | <<jsr305,New-Style BSD>>
-|dk.brics.automaton         | <<automaton,New-Style BSD>>
-|Java Concurrency in Practice Annotations | <<jcip,Create Commons Attribution License>>
-|pegdown                    | <<apache2,Apache License 2.0>>
-|======================================================================
-
-Cryptography Notice
--------------------
-
-This distribution includes cryptographic software.  The country
-in which you currently reside may have restrictions on the import,
-possession, use, and/or re-export to another country, of encryption
-software.  BEFORE using any encryption software, please check
-your country's laws, regulations and policies concerning the
-import, possession, or use, and re-export of encryption software,
-to see if this is permitted.  See the
-link:http://www.wassenaar.org/[Wassenaar Arrangement]
-for more information.
-
-The U.S. Government Department of Commerce, Bureau of Industry
-and Security (BIS), has classified this software as Export
-Commodity Control Number (ECCN) 5D002.C.1, which includes
-information security software using or performing cryptographic
-functions with asymmetric algorithms.  The form and manner of
-this distribution makes it eligible for export under the License
-Exception ENC Technology Software Unrestricted (TSU) exception
-(see the BIS Export Administration Regulations, Section 740.13)
-for both object code and source code.
-
-Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
-uploads of changes directly from `git push` command line clients.
-
-Gerrit includes an SSH client (JSch), to support authenticated
-replication of changes to remote systems, such as for automatic
-updates of mirror servers, or realtime backups.
-
-For either feature to function, Gerrit requires the
-link:http://java.sun.com/javase/technologies/security/[Java Cryptography extensions]
-and/or the
-link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
-to be installed by the end-user.
-
-Licenses
---------
-
-[[apache2]]
-Apache License 2.0
-~~~~~~~~~~~~~~~~~~
-
-----
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-----
-
-[[sshd]]
-Apache SSHD - Notice
-~~~~~~~~~~~~~~~~~~~~
-
-* link:http://svn.apache.org/viewvc/mina/sshd/trunk/NOTICE.txt?view=markup[Original]
-
-----
-   =========================================================================
-   ==  NOTICE file for use with the Apache License, Version 2.0,          ==
-   ==  in this case for the SSHD distribution.                            ==
-   =========================================================================
-
-   This product contains software developped by JCraft,Inc. and subject to
-   the following license:
-
-Copyright (c) 2002,2003,2004,2005,2006,2007,2008 Atsuhiko Yamanaka, JCraft,Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-  1. Redistributions of source code must retain the above copyright notice,
-     this list of conditions and the following disclaimer.
-
-  2. Redistributions in binary form must reproduce the above copyright
-     notice, this list of conditions and the following disclaimer in
-     the documentation and/or other materials provided with the distribution.
-
-  3. The names of the authors may not be used to endorse or promote products
-     derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
-INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
-EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
- --------------------------------------------------------------------------------
-
-Copyright (c) 2000 - 2006 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in the
-Software without restriction, including without limitation the rights to use, copy,
-modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
-and to permit persons to whom the Software is furnished to do so, subject to the
-following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----
-
-[[postgresql]]
-PostgreSQL JDBC Driver - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://jdbc.postgresql.org/license.html[Original]
-
-----
-Copyright (c) 1997-2008, PostgreSQL Global Development Group
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice,
-   this list of conditions and the following disclaimer.
-2. Redistributions in binary form must reproduce the above copyright notice,
-   this list of conditions and the following disclaimer in the documentation
-   and/or other materials provided with the distribution.
-3. Neither the name of the PostgreSQL Global Development Group nor the names
-   of its contributors may be used to endorse or promote products derived
-   from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[prolog_cafe]]
-Prolog Cafe - EPL or GPL
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Originally developed by Mutsunori BANBARA and Naoyuki TAMURA at the
-Kobe University, JAPAN. Gerrit Code Review uses a fork derived from
-the 1.2.5 release, and offers the corresponding source code at
-link:https://gerrit.googlesource.com/prolog-cafe[].
-
-Prolog Cafe is dual licensed and available under either the
-link:http://opensource.org/licenses/eclipse-1.0.php[Eclipse Public License],
-or the
-link:http://www.gnu.org/licenses/gpl-2.0.html[GPL version 2.0 (or later)].
-
-In the context of Gerrit Code Review, Prolog Cafe is consumed
-under the EPL.
-
-[[h2]]
-H2 Database - EPL or modified MPL
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.h2database.com/html/license.html[Complete Terms]
-
-H2 is dual licensed and available under a modified version of the
-MPL 1.1 (Mozilla Public License) or under the (unmodified) EPL 1.0
-(link:http://opensource.org/licenses/eclipse-1.0.php[Eclipse Public License]).
-
-[[asm]]
-ObjectWeb ASM - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://svn.forge.objectweb.org/cgi-bin/viewcvs.cgi/asm/trunk/asm/LICENSE.txt[Original]
-
-----
- ASM: a very small and fast Java bytecode manipulation framework
- Copyright (c) 2000-2005 INRIA, France Telecom
- All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions
- are met:
-
- 1. Redistributions of source code must retain the above copyright
-    notice, this list of conditions and the following disclaimer.
- 2. Redistributions in binary form must reproduce the above copyright
-    notice, this list of conditions and the following disclaimer in the
-    documentation and/or other materials provided with the distribution.
- 3. Neither the name of the copyright holders nor the names of its
-    contributors may be used to endorse or promote products derived from
-    this software without specific prior written permission.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
- THE POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[antlr]]
-ANTLR - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.antlr.org/license.html[Original]
-
-----
-Copyright (c) 2003-2008, Terence Parr
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-    * Redistributions of source code must retain the above copyright
-      notice, this list of conditions and the following disclaimer.
-    * Redistributions in binary form must reproduce the above
-      copyright notice, this list of conditions and the following
-      disclaimer in the documentation and/or other materials provided
-      with the distribution.
-    * Neither the name of the author nor the names of its
-      contributors may be used to endorse or promote products derived
-      from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[jgit]]
-JGit - New Style BSD
-~~~~~~~~~~~~~~~~~~~~
-
-* link:http://repo.or.cz/w/egit.git?a=blob;f=org.spearce.jgit/LICENSE;hb=HEAD[Original]
-
-----
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- *
- * - Neither the name of the Git Development Community nor the
- *   names of its contributors may be used to endorse or promote
- *   products derived from this software without specific prior
- *   written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[jsr305]]
-JSR 305 Reference Implementation - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://code.google.com/p/jsr-305/source/browse/trunk/ri/LICENSE[Original 1]
-* link:http://code.google.com/p/findbugs/source/browse/trunk/findbugs/LICENSE-jsr305.txt[Original 2]
-
-----
-Copyright (c) 2007-2009, JSR305 expert group
-All rights reserved.
-
-http://www.opensource.org/licenses/bsd-license.php
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-    * Redistributions of source code must retain the above copyright notice,
-      this list of conditions and the following disclaimer.
-    * Redistributions in binary form must reproduce the above copyright notice,
-      this list of conditions and the following disclaimer in the documentation
-      and/or other materials provided with the distribution.
-    * Neither the name of the JSR305 expert group nor the names of its
-      contributors may be used to endorse or promote products derived from
-      this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[automaton]]
-dk.brics.automaton - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.brics.dk/automaton/index.html
-
-----
-Copyright (c) 2007-2009, dk.brics.automaton
-All rights reserved.
-
-http://www.opensource.org/licenses/bsd-license.php
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-    * Redistributions of source code must retain the above copyright notice,
-      this list of conditions and the following disclaimer.
-    * Redistributions in binary form must reproduce the above copyright notice,
-      this list of conditions and the following disclaimer in the documentation
-      and/or other materials provided with the distribution.
-    * Neither the name of the JSR305 expert group nor the names of its
-      contributors may be used to endorse or promote products derived from
-      this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[args4j]]
-args4j - MIT License
-~~~~~~~~~~~~~~~~~~~~
-
-* link:https://args4j.dev.java.net/[Home]
-* link:http://www.opensource.org/licenses/mit-license.php[Canonical MIT License]
-
-[[slf4j]]
-SLF4J - MIT License
-~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.slf4j.org/license.html[Original]
-
-----
- Copyright (c) 2004-2008 QOS.ch
- All rights reserved.
-
- Permission is hereby granted, free  of charge, to any person obtaining
- a  copy  of this  software  and  associated  documentation files  (the
- "Software"), to  deal in  the Software without  restriction, including
- without limitation  the rights to  use, copy, modify,  merge, publish,
- distribute,  sublicense, and/or sell  copies of  the Software,  and to
- permit persons to whom the Software  is furnished to do so, subject to
- the following conditions:
-
- The  above  copyright  notice  and  this permission  notice  shall  be
- included in all copies or substantial portions of the Software.
-
- THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
- EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
- MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----
-
-[[clippy]]
-Clippy - MIT License
-~~~~~~~~~~~~~~~~~~~~
-
-* link:http://github.com/mojombo/clippy/tree/master[Site]
-
-----
-(The MIT License)
-
-Copyright (c) 2008 Tom Preston-Werner
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
- 
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
- 
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----
-
-[[jcip]]
-Java Concurrency in Practice Annotations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://jcip.net/[book website]
-* link:http://jcip.net/jcip-annotations-src.jar[sources]
-* link:http://creativecommons.org/licenses/by/2.5/[license]
-
-----
-Copyright (c) 2005 Brian Goetz and Tim Peierls
-Released under the Creative Commons Attribution License
-  (http://creativecommons.org/licenses/by/2.5)
-Official home: http://www.jcip.net
-
-Any republication or derived work distributed in source code form
-must include this copyright and license notice.
-----
-
-[[mpl1_1]]
-Mozilla Public License 1.1
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.mozilla.org/MPL/MPL-1.1.html[Mozilla Public License (Original)]
-* link:http://www.mozilla.org/MPL/MPL-1.1-annotated.html[Mozilla Public License (Annotated)]
-
-----
-                          MOZILLA PUBLIC LICENSE
-                                Version 1.1
-
-                              ---------------
-
-1. Definitions.
-
-     1.0.1. "Commercial Use" means distribution or otherwise making the
-     Covered Code available to a third party.
-
-     1.1. "Contributor" means each entity that creates or contributes to
-     the creation of Modifications.
-
-     1.2. "Contributor Version" means the combination of the Original
-     Code, prior Modifications used by a Contributor, and the Modifications
-     made by that particular Contributor.
-
-     1.3. "Covered Code" means the Original Code or Modifications or the
-     combination of the Original Code and Modifications, in each case
-     including portions thereof.
-
-     1.4. "Electronic Distribution Mechanism" means a mechanism generally
-     accepted in the software development community for the electronic
-     transfer of data.
-
-     1.5. "Executable" means Covered Code in any form other than Source
-     Code.
-
-     1.6. "Initial Developer" means the individual or entity identified
-     as the Initial Developer in the Source Code notice required by Exhibit
-     A.
-
-     1.7. "Larger Work" means a work which combines Covered Code or
-     portions thereof with code not governed by the terms of this License.
-
-     1.8. "License" means this document.
-
-     1.8.1. "Licensable" means having the right to grant, to the maximum
-     extent possible, whether at the time of the initial grant or
-     subsequently acquired, any and all of the rights conveyed herein.
-
-     1.9. "Modifications" means any addition to or deletion from the
-     substance or structure of either the Original Code or any previous
-     Modifications. When Covered Code is released as a series of files, a
-     Modification is:
-          A. Any addition to or deletion from the contents of a file
-          containing Original Code or previous Modifications.
-
-          B. Any new file that contains any part of the Original Code or
-          previous Modifications.
-
-     1.10. "Original Code" means Source Code of computer software code
-     which is described in the Source Code notice required by Exhibit A as
-     Original Code, and which, at the time of its release under this
-     License is not already Covered Code governed by this License.
-
-     1.10.1. "Patent Claims" means any patent claim(s), now owned or
-     hereafter acquired, including without limitation,  method, process,
-     and apparatus claims, in any patent Licensable by grantor.
-
-     1.11. "Source Code" means the preferred form of the Covered Code for
-     making modifications to it, including all modules it contains, plus
-     any associated interface definition files, scripts used to control
-     compilation and installation of an Executable, or source code
-     differential comparisons against either the Original Code or another
-     well known, available Covered Code of the Contributor's choice. The
-     Source Code can be in a compressed or archival form, provided the
-     appropriate decompression or de-archiving software is widely available
-     for no charge.
-
-     1.12. "You" (or "Your")  means an individual or a legal entity
-     exercising rights under, and complying with all of the terms of, this
-     License or a future version of this License issued under Section 6.1.
-     For legal entities, "You" includes any entity which controls, is
-     controlled by, or is under common control with You. For purposes of
-     this definition, "control" means (a) the power, direct or indirect,
-     to cause the direction or management of such entity, whether by
-     contract or otherwise, or (b) ownership of more than fifty percent
-     (50%) of the outstanding shares or beneficial ownership of such
-     entity.
-
-2. Source Code License.
-
-     2.1. The Initial Developer Grant.
-     The Initial Developer hereby grants You a world-wide, royalty-free,
-     non-exclusive license, subject to third party intellectual property
-     claims:
-          (a)  under intellectual property rights (other than patent or
-          trademark) Licensable by Initial Developer to use, reproduce,
-          modify, display, perform, sublicense and distribute the Original
-          Code (or portions thereof) with or without Modifications, and/or
-          as part of a Larger Work; and
-
-          (b) under Patents Claims infringed by the making, using or
-          selling of Original Code, to make, have made, use, practice,
-          sell, and offer for sale, and/or otherwise dispose of the
-          Original Code (or portions thereof).
-
-          (c) the licenses granted in this Section 2.1(a) and (b) are
-          effective on the date Initial Developer first distributes
-          Original Code under the terms of this License.
-
-          (d) Notwithstanding Section 2.1(b) above, no patent license is
-          granted: 1) for code that You delete from the Original Code; 2)
-          separate from the Original Code;  or 3) for infringements caused
-          by: i) the modification of the Original Code or ii) the
-          combination of the Original Code with other software or devices.
-
-     2.2. Contributor Grant.
-     Subject to third party intellectual property claims, each Contributor
-     hereby grants You a world-wide, royalty-free, non-exclusive license
-
-          (a)  under intellectual property rights (other than patent or
-          trademark) Licensable by Contributor, to use, reproduce, modify,
-          display, perform, sublicense and distribute the Modifications
-          created by such Contributor (or portions thereof) either on an
-          unmodified basis, with other Modifications, as Covered Code
-          and/or as part of a Larger Work; and
-
-          (b) under Patent Claims infringed by the making, using, or
-          selling of  Modifications made by that Contributor either alone
-          and/or in combination with its Contributor Version (or portions
-          of such combination), to make, use, sell, offer for sale, have
-          made, and/or otherwise dispose of: 1) Modifications made by that
-          Contributor (or portions thereof); and 2) the combination of
-          Modifications made by that Contributor with its Contributor
-          Version (or portions of such combination).
-
-          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
-          effective on the date Contributor first makes Commercial Use of
-          the Covered Code.
-
-          (d)    Notwithstanding Section 2.2(b) above, no patent license is
-          granted: 1) for any code that Contributor has deleted from the
-          Contributor Version; 2)  separate from the Contributor Version;
-          3)  for infringements caused by: i) third party modifications of
-          Contributor Version or ii)  the combination of Modifications made
-          by that Contributor with other software  (except as part of the
-          Contributor Version) or other devices; or 4) under Patent Claims
-          infringed by Covered Code in the absence of Modifications made by
-          that Contributor.
-
-3. Distribution Obligations.
-
-     3.1. Application of License.
-     The Modifications which You create or to which You contribute are
-     governed by the terms of this License, including without limitation
-     Section 2.2. The Source Code version of Covered Code may be
-     distributed only under the terms of this License or a future version
-     of this License released under Section 6.1, and You must include a
-     copy of this License with every copy of the Source Code You
-     distribute. You may not offer or impose any terms on any Source Code
-     version that alters or restricts the applicable version of this
-     License or the recipients' rights hereunder. However, You may include
-     an additional document offering the additional rights described in
-     Section 3.5.
-
-     3.2. Availability of Source Code.
-     Any Modification which You create or to which You contribute must be
-     made available in Source Code form under the terms of this License
-     either on the same media as an Executable version or via an accepted
-     Electronic Distribution Mechanism to anyone to whom you made an
-     Executable version available; and if made available via Electronic
-     Distribution Mechanism, must remain available for at least twelve (12)
-     months after the date it initially became available, or at least six
-     (6) months after a subsequent version of that particular Modification
-     has been made available to such recipients. You are responsible for
-     ensuring that the Source Code version remains available even if the
-     Electronic Distribution Mechanism is maintained by a third party.
-
-     3.3. Description of Modifications.
-     You must cause all Covered Code to which You contribute to contain a
-     file documenting the changes You made to create that Covered Code and
-     the date of any change. You must include a prominent statement that
-     the Modification is derived, directly or indirectly, from Original
-     Code provided by the Initial Developer and including the name of the
-     Initial Developer in (a) the Source Code, and (b) in any notice in an
-     Executable version or related documentation in which You describe the
-     origin or ownership of the Covered Code.
-
-     3.4. Intellectual Property Matters
-          (a) Third Party Claims.
-          If Contributor has knowledge that a license under a third party's
-          intellectual property rights is required to exercise the rights
-          granted by such Contributor under Sections 2.1 or 2.2,
-          Contributor must include a text file with the Source Code
-          distribution titled "LEGAL" which describes the claim and the
-          party making the claim in sufficient detail that a recipient will
-          know whom to contact. If Contributor obtains such knowledge after
-          the Modification is made available as described in Section 3.2,
-          Contributor shall promptly modify the LEGAL file in all copies
-          Contributor makes available thereafter and shall take other steps
-          (such as notifying appropriate mailing lists or newsgroups)
-          reasonably calculated to inform those who received the Covered
-          Code that new knowledge has been obtained.
-
-          (b) Contributor APIs.
-          If Contributor's Modifications include an application programming
-          interface and Contributor has knowledge of patent licenses which
-          are reasonably necessary to implement that API, Contributor must
-          also include this information in the LEGAL file.
-
-               (c)    Representations.
-          Contributor represents that, except as disclosed pursuant to
-          Section 3.4(a) above, Contributor believes that Contributor's
-          Modifications are Contributor's original creation(s) and/or
-          Contributor has sufficient rights to grant the rights conveyed by
-          this License.
-
-     3.5. Required Notices.
-     You must duplicate the notice in Exhibit A in each file of the Source
-     Code.  If it is not possible to put such notice in a particular Source
-     Code file due to its structure, then You must include such notice in a
-     location (such as a relevant directory) where a user would be likely
-     to look for such a notice.  If You created one or more Modification(s)
-     You may add your name as a Contributor to the notice described in
-     Exhibit A.  You must also duplicate this License in any documentation
-     for the Source Code where You describe recipients' rights or ownership
-     rights relating to Covered Code.  You may choose to offer, and to
-     charge a fee for, warranty, support, indemnity or liability
-     obligations to one or more recipients of Covered Code. However, You
-     may do so only on Your own behalf, and not on behalf of the Initial
-     Developer or any Contributor. You must make it absolutely clear than
-     any such warranty, support, indemnity or liability obligation is
-     offered by You alone, and You hereby agree to indemnify the Initial
-     Developer and every Contributor for any liability incurred by the
-     Initial Developer or such Contributor as a result of warranty,
-     support, indemnity or liability terms You offer.
-
-     3.6. Distribution of Executable Versions.
-     You may distribute Covered Code in Executable form only if the
-     requirements of Section 3.1-3.5 have been met for that Covered Code,
-     and if You include a notice stating that the Source Code version of
-     the Covered Code is available under the terms of this License,
-     including a description of how and where You have fulfilled the
-     obligations of Section 3.2. The notice must be conspicuously included
-     in any notice in an Executable version, related documentation or
-     collateral in which You describe recipients' rights relating to the
-     Covered Code. You may distribute the Executable version of Covered
-     Code or ownership rights under a license of Your choice, which may
-     contain terms different from this License, provided that You are in
-     compliance with the terms of this License and that the license for the
-     Executable version does not attempt to limit or alter the recipient's
-     rights in the Source Code version from the rights set forth in this
-     License. If You distribute the Executable version under a different
-     license You must make it absolutely clear that any terms which differ
-     from this License are offered by You alone, not by the Initial
-     Developer or any Contributor. You hereby agree to indemnify the
-     Initial Developer and every Contributor for any liability incurred by
-     the Initial Developer or such Contributor as a result of any such
-     terms You offer.
-
-     3.7. Larger Works.
-     You may create a Larger Work by combining Covered Code with other code
-     not governed by the terms of this License and distribute the Larger
-     Work as a single product. In such a case, You must make sure the
-     requirements of this License are fulfilled for the Covered Code.
-
-4. Inability to Comply Due to Statute or Regulation.
-
-     If it is impossible for You to comply with any of the terms of this
-     License with respect to some or all of the Covered Code due to
-     statute, judicial order, or regulation then You must: (a) comply with
-     the terms of this License to the maximum extent possible; and (b)
-     describe the limitations and the code they affect. Such description
-     must be included in the LEGAL file described in Section 3.4 and must
-     be included with all distributions of the Source Code. Except to the
-     extent prohibited by statute or regulation, such description must be
-     sufficiently detailed for a recipient of ordinary skill to be able to
-     understand it.
-
-5. Application of this License.
-
-     This License applies to code to which the Initial Developer has
-     attached the notice in Exhibit A and to related Covered Code.
-
-6. Versions of the License.
-
-     6.1. New Versions.
-     Netscape Communications Corporation ("Netscape") may publish revised
-     and/or new versions of the License from time to time. Each version
-     will be given a distinguishing version number.
-
-     6.2. Effect of New Versions.
-     Once Covered Code has been published under a particular version of the
-     License, You may always continue to use it under the terms of that
-     version. You may also choose to use such Covered Code under the terms
-     of any subsequent version of the License published by Netscape. No one
-     other than Netscape has the right to modify the terms applicable to
-     Covered Code created under this License.
-
-     6.3. Derivative Works.
-     If You create or use a modified version of this License (which you may
-     only do in order to apply it to code which is not already Covered Code
-     governed by this License), You must (a) rename Your license so that
-     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
-     "MPL", "NPL" or any confusingly similar phrase do not appear in your
-     license (except to note that your license differs from this License)
-     and (b) otherwise make it clear that Your version of the license
-     contains terms which differ from the Mozilla Public License and
-     Netscape Public License. (Filling in the name of the Initial
-     Developer, Original Code or Contributor in the notice described in
-     Exhibit A shall not of themselves be deemed to be modifications of
-     this License.)
-
-7. DISCLAIMER OF WARRANTY.
-
-     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
-     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
-     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
-     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
-     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
-     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
-     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
-     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
-     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
-     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
-
-8. TERMINATION.
-
-     8.1.  This License and the rights granted hereunder will terminate
-     automatically if You fail to comply with terms herein and fail to cure
-     such breach within 30 days of becoming aware of the breach. All
-     sublicenses to the Covered Code which are properly granted shall
-     survive any termination of this License. Provisions which, by their
-     nature, must remain in effect beyond the termination of this License
-     shall survive.
-
-     8.2.  If You initiate litigation by asserting a patent infringement
-     claim (excluding declatory judgment actions) against Initial Developer
-     or a Contributor (the Initial Developer or Contributor against whom
-     You file such action is referred to as "Participant")  alleging that:
-
-     (a)  such Participant's Contributor Version directly or indirectly
-     infringes any patent, then any and all rights granted by such
-     Participant to You under Sections 2.1 and/or 2.2 of this License
-     shall, upon 60 days notice from Participant terminate prospectively,
-     unless if within 60 days after receipt of notice You either: (i)
-     agree in writing to pay Participant a mutually agreeable reasonable
-     royalty for Your past and future use of Modifications made by such
-     Participant, or (ii) withdraw Your litigation claim with respect to
-     the Contributor Version against such Participant.  If within 60 days
-     of notice, a reasonable royalty and payment arrangement are not
-     mutually agreed upon in writing by the parties or the litigation claim
-     is not withdrawn, the rights granted by Participant to You under
-     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
-     the 60 day notice period specified above.
-
-     (b)  any software, hardware, or device, other than such Participant's
-     Contributor Version, directly or indirectly infringes any patent, then
-     any rights granted to You by such Participant under Sections 2.1(b)
-     and 2.2(b) are revoked effective as of the date You first made, used,
-     sold, distributed, or had made, Modifications made by that
-     Participant.
-
-     8.3.  If You assert a patent infringement claim against Participant
-     alleging that such Participant's Contributor Version directly or
-     indirectly infringes any patent where such claim is resolved (such as
-     by license or settlement) prior to the initiation of patent
-     infringement litigation, then the reasonable value of the licenses
-     granted by such Participant under Sections 2.1 or 2.2 shall be taken
-     into account in determining the amount or value of any payment or
-     license.
-
-     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
-     all end user license agreements (excluding distributors and resellers)
-     which have been validly granted by You or any distributor hereunder
-     prior to termination shall survive termination.
-
-9. LIMITATION OF LIABILITY.
-
-     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
-     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
-     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
-     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
-     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
-     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
-     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
-     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
-     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
-     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
-     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
-     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
-     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
-     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
-
-10. U.S. GOVERNMENT END USERS.
-
-     The Covered Code is a "commercial item," as that term is defined in
-     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
-     software" and "commercial computer software documentation," as such
-     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
-     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
-     all U.S. Government End Users acquire Covered Code with only those
-     rights set forth herein.
-
-11. MISCELLANEOUS.
-
-     This License represents the complete agreement concerning subject
-     matter hereof. If any provision of this License is held to be
-     unenforceable, such provision shall be reformed only to the extent
-     necessary to make it enforceable. This License shall be governed by
-     California law provisions (except to the extent applicable law, if
-     any, provides otherwise), excluding its conflict-of-law provisions.
-     With respect to disputes in which at least one party is a citizen of,
-     or an entity chartered or registered to do business in the United
-     States of America, any litigation relating to this License shall be
-     subject to the jurisdiction of the Federal Courts of the Northern
-     District of California, with venue lying in Santa Clara County,
-     California, with the losing party responsible for costs, including
-     without limitation, court costs and reasonable attorneys' fees and
-     expenses. The application of the United Nations Convention on
-     Contracts for the International Sale of Goods is expressly excluded.
-     Any law or regulation which provides that the language of a contract
-     shall be construed against the drafter shall not apply to this
-     License.
-
-12. RESPONSIBILITY FOR CLAIMS.
-
-     As between Initial Developer and the Contributors, each party is
-     responsible for claims and damages arising, directly or indirectly,
-     out of its utilization of rights under this License and You agree to
-     work with Initial Developer and Contributors to distribute such
-     responsibility on an equitable basis. Nothing herein is intended or
-     shall be deemed to constitute any admission of liability.
-
-13. MULTIPLE-LICENSED CODE.
-
-     Initial Developer may designate portions of the Covered Code as
-     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
-     Developer permits you to utilize portions of the Covered Code under
-     Your choice of the NPL or the alternative licenses, if any, specified
-     by the Initial Developer in the file described in Exhibit A.
-
-EXHIBIT A -Mozilla Public License.
-
-     ``The contents of this file are subject to the Mozilla Public License
-     Version 1.1 (the "License"); you may not use this file except in
-     compliance with the License. You may obtain a copy of the License at
-     http://www.mozilla.org/MPL/
-
-     Software distributed under the License is distributed on an "AS IS"
-     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
-     License for the specific language governing rights and limitations
-     under the License.
-
-     The Original Code is ______________________________________.
-
-     The Initial Developer of the Original Code is ________________________.
-     Portions created by ______________________ are Copyright (C) ______
-     _______________________. All Rights Reserved.
-
-     Contributor(s): ______________________________________.
-
-     Alternatively, the contents of this file may be used under the terms
-     of the _____ license (the  "[___] License"), in which case the
-     provisions of [______] License are applicable instead of those
-     above.  If you wish to allow use of your version of this file only
-     under the terms of the [____] License and not to allow others to use
-     your version of this file under the MPL, indicate your decision by
-     deleting  the provisions above and replace  them with the notice and
-     other provisions required by the [___] License.  If you do not delete
-     the provisions above, a recipient may use your version of this file
-     under either the MPL or the [___] License."
-
-     [NOTE: The text of this Exhibit A may differ slightly from the text of
-     the notices in the Source Code files of the Original Code. You should
-     use the text of this Exhibit A rather than the text found in the
-     Original Code Source Code for Your Modifications.]
-----
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/man/Makefile b/Documentation/man/Makefile
index 75e6533..945f215 100644
--- a/Documentation/man/Makefile
+++ b/Documentation/man/Makefile
@@ -34,6 +34,7 @@
 cmd-rename-group.txt   \
 cmd-review.txt         \
 cmd-set-account.txt    \
+cmd-set-members.txt    \
 cmd-set-project-parent.txt \
 cmd-set-project.txt    \
 cmd-set-reviewers.txt  \
diff --git a/Documentation/pgm-ScanTrackingIds.txt b/Documentation/pgm-ScanTrackingIds.txt
index ea5d72e..f9494d4 100644
--- a/Documentation/pgm-ScanTrackingIds.txt
+++ b/Documentation/pgm-ScanTrackingIds.txt
@@ -20,6 +20,14 @@
 concurrently to the server if the database is MySQL or PostgreSQL.
 If the database is H2, this task must be run by itself.
 
+STATUS
+------
+This command will be replaced by `reindex`.
+
+If secondary indexing is enabled
+(link:config-gerrit.html#index.type[index.type] set to `LUCENE`
+or `SOLR`) use link:pgm-reindex.html[reindex].
+
 OPTIONS
 -------
 
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 3ffcb40..ce88ab7 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -15,6 +15,7 @@
 	[\--console-log]
 	[\--slave]
 	[\--headless]
+	[\--init]
 
 DESCRIPTION
 -----------
@@ -63,6 +64,10 @@
 	Don't start the default Gerrit UI. May be useful when Gerrit is
 	run with an alternative UI.
 
+\--init::
+	Run init before starting the daemon. This will create a new site or
+	upgrade an existing site.
+
 CONTEXT
 -------
 This command can only be run on a server which has direct
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 7a1edcf..987b4ac 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -23,6 +23,9 @@
 link:pgm-prolog-shell.html[prolog-shell]::
 	Simple interactive Prolog interpreter.
 
+link:pgm-reindex.html[reindex]::
+	Rebuild the secondary index.
+
 link:pgm-rulec.html[rulec]::
 	Compile project-specific Prolog rules to JARs.
 
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 57decdd..3d6cb73 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -12,6 +12,8 @@
 	-d <SITE_PATH>
 	[\--batch]
 	[\--no-auto-start]
+	[\--list-plugins]
+	[\--install-plugin=<PLUGIN_NAME>]
 
 DESCRIPTION
 -----------
@@ -45,6 +47,12 @@
 	Location of the gerrit.config file, and all other per-site
 	configuration data, supporting libraries and log files.
 
+\--list-plugins::
+	Print names of plugins that can be installed during init process.
+
+\--install-plugin:
+	Automatically install plugin with given name without asking.
+
 CONTEXT
 -------
 This command can only be run on a server which has direct
diff --git a/Documentation/pgm-prolog-shell.txt b/Documentation/pgm-prolog-shell.txt
index f3fa2d8..3189e90 100644
--- a/Documentation/pgm-prolog-shell.txt
+++ b/Documentation/pgm-prolog-shell.txt
@@ -20,7 +20,7 @@
 -s::
 	Dynamically load the Prolog source code at startup,
 	as though the user had entered `['FILE.pl'].` into
-	the interepter once it was running. This option may
+	the interpreter once it was running. This option may
 	be supplied more than once to load multiple files.
 
 EXAMPLES
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
new file mode 100644
index 0000000..2b44f6b
--- /dev/null
+++ b/Documentation/pgm-reindex.txt
@@ -0,0 +1,41 @@
+reindex
+=======
+
+NAME
+----
+reindex - Rebuild the secondary index
+
+SYNOPSIS
+--------
+[verse]
+'java' -jar gerrit.war 'reindex' [<OPTIONS>]
+
+DESCRIPTION
+-----------
+Rebuilds the secondary index.
+
+OPTIONS
+-------
+--threads::
+	Number of threads to use for indexing.
+
+--schema-version::
+	Schema version to reindex; default is most recent version.
+
+--output::
+	Prefix for output; path for local disk index, or prefix for remote index.
+
+--verbose::
+	Output debug information for each change.
+
+--dry-run::
+	Dry run.  Don't write anything to index.
+
+CONTEXT
+-------
+The secondary index must be enabled. See
+link:config-gerrit.html#index.type[index.type].
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 170d11a..b1710a2 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -182,7 +182,7 @@
 <2> label `'Verified'` is rejected. Change is not submittable.
 <3> label `'Author-is-John-Doe'` is needed for the change to become submittable.
     Note that this tells nothing about how this criteria will be met. It is up
-    to the implementor of the `submit_rule` to return `label('Author-is-John-Doe',
+    to the implementer of the `submit_rule` to return `label('Author-is-John-Doe',
     ok(_))` when this criteria is met.  Most likely, it will have to match
     against `gerrit:commit_author` in order to check if this criteria is met.
     This will become clear through the examples below.
@@ -645,7 +645,7 @@
 
 Reusing the default submit policy
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-To get results of Gerrits default submit policy we use the
+To get results of Gerrit's default submit policy we use the
 `gerrit:default_submit` predicate.  The `gerrit:default_submit(X)` includes all
 categories from the database.  This means that if we write a submit rule like:
 
@@ -743,7 +743,7 @@
 The latter implementation is probably easier to understand and the code looks
 cleaner. Note, however, that the latter implementation will always return the
 two standard categories only (`Code-Review` and `Verified`) even if a new
-category has beeen inserted into the database. To include the new category
+category has been inserted into the database. To include the new category
 the `rules.pl` would need to be modified or a `submit_filter` in a parent
 project would have to care about including the new category in the result
 of this `submit_rule`.
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
new file mode 100755
index 0000000..1680a47
--- /dev/null
+++ b/Documentation/replace_macros.py
@@ -0,0 +1,106 @@
+#!/usr/bin/python
+# 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.
+
+from optparse import OptionParser
+import re
+import sys
+
+PAT_GERRIT = re.compile(r'^GERRIT')
+PAT_INCLUDE = re.compile(r'^(include::.*)(\[\])$')
+PAT_GET = re.compile(r'^get::([^ \t\n]*)')
+PAT_TITLE = re.compile(r'^\.(.*)')
+PAT_STARS = re.compile(r'^\*\*\*\*')
+
+GERRIT_UPLINK = """
+
+++++
+<hr style=\"
+  height: 2px;
+  color: silver;
+  margin-top: 1.2em;
+  margin-bottom: 0.5em;
+\">
+++++
+
+"""
+
+GET_TITLE = '<div class="title">%s</div>'
+
+GET_MACRO = """
+
+++++
+<div class="listingblock">
+%s
+<div class="content">
+<a id=\"{0}\" onmousedown="javascript:
+  var i = document.URL.lastIndexOf(\'/Documentation/\');
+  var url = document.URL.substring(0, i) + \'{0}\';
+  document.getElementById(\'{0}\').href = url;">
+    GET {0} HTTP/1.0
+</a>
+</div>
+</div>
+++++
+
+"""
+
+opts = OptionParser()
+opts.add_option('-o', '--out', help='output file')
+opts.add_option('-s', '--src', help='source file')
+opts.add_option('-x', '--suffix', help='suffix for included filenames')
+options, _ = opts.parse_args()
+
+try:
+  out_file = open(options.out, 'w')
+  src_file = open(options.src, 'r')
+  last_line = ''
+  ignore_next_line = False
+  last_title = ''
+  for line in src_file.xreadlines():
+    if PAT_GERRIT.match(last_line):
+      # Case of "GERRIT\n------" at the footer
+      out_file.write(GERRIT_UPLINK)
+      last_line = ''
+    elif PAT_INCLUDE.match(line):
+      # Case of 'include::<filename>'
+      match = PAT_INCLUDE.match(line)
+      out_file.write(last_line)
+      last_line = match.group(1) + options.suffix + match.group(2) + '\n'
+    elif PAT_STARS.match(line):
+      if PAT_TITLE.match(last_line):
+        # Case of the title in '.<title>\n****\nget::<url>\n****'
+        match = PAT_TITLE.match(last_line)
+        last_title = GET_TITLE % match.group(1)
+      else:
+        out_file.write(last_line)
+        last_title = ''
+    elif PAT_GET.match(line):
+      # Case of '****\nget::<url>\n****' in rest api
+      url = PAT_GET.match(line).group(1)
+      out_file.write(GET_MACRO.format(url) % last_title)
+      ignore_next_line = True
+    elif ignore_next_line:
+      # Handle the trailing '****' of the 'get::' case
+      last_line = ''
+      ignore_next_line = False
+    else:
+      out_file.write(last_line)
+      last_line = line
+  out_file.write(last_line)
+  out_file.close()
+except IOError as err:
+  sys.stderr.write(
+      "error while expanding %s to %s: %s" % (options.src, options.out, err))
+  exit(1)
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
new file mode 100644
index 0000000..6c786e4
--- /dev/null
+++ b/Documentation/rest-api-access.txt
@@ -0,0 +1,380 @@
+Gerrit Code Review - /access/ REST API
+======================================
+
+This page describes the access rights related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[access-endpoints]]
+Access Rights Endpoints
+-----------------------
+
+[[list-access]]
+List Access Rights
+~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /access/?project=link:rest-api-projects.html#project-name[\{project-name\}]'
+
+Lists the access rights for projects. The projects for which the access
+rights should be returned must be specified as `project` options. The
+`project` can be specified multiple times.
+
+As result a map is returned that maps the project name to
+link:#project-access-info[ProjectAccessInfo] entities.
+
+The entries in the map are sorted by project name.
+
+.Request
+----
+  GET /access/?project=MyProject&project=All-Projects HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "All-Projects": {
+      "revision": "edd453d18e08640e67a8c9a150cec998ed0ac9aa",
+      "local": {
+        "GLOBAL_CAPABILITIES": {
+          "permissions": {
+            "priority": {
+              "rules": {
+                "15bfcd8a6de1a69c50b30cedcdcc951c15703152": {
+                  "action": "BATCH"
+                }
+              }
+            },
+            "streamEvents": {
+              "rules": {
+                "15bfcd8a6de1a69c50b30cedcdcc951c15703152": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "administrateServer": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                }
+              }
+            }
+          }
+        },
+        "refs/meta/config": {
+          "permissions": {
+            "submit": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "label-Code-Review": {
+              "label": "Code-Review",
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW",
+                  "min": -2,
+                  "max": 2
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW",
+                  "min": -2,
+                  "max": 2
+                }
+              }
+            },
+            "read": {
+              "exclusive": true,
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "push": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            }
+          }
+        },
+        "refs/for/refs/*": {
+          "permissions": {
+            "pushMerge": {
+              "rules": {
+                "global:Registered-Users": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "push": {
+              "rules": {
+                "global:Registered-Users": {
+                  "action": "ALLOW"
+                }
+              }
+            }
+          }
+        },
+        "refs/tags/*": {
+          "permissions": {
+            "pushSignedTag": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "pushTag": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            }
+          }
+        },
+        "refs/heads/*": {
+          "permissions": {
+            "forgeCommitter": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "forgeAuthor": {
+              "rules": {
+                "global:Registered-Users": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "submit": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "editTopicName": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW",
+                  "force": true
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW",
+                  "force": true
+                }
+              }
+            },
+            "label-Code-Review": {
+              "label": "Code-Review",
+              "rules": {
+                "global:Registered-Users": {
+                  "action": "ALLOW",
+                  "min": -1,
+                  "max": 1
+                },
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW",
+                  "min": -2,
+                  "max": 2
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW",
+                  "min": -2,
+                  "max": 2
+                }
+              }
+            },
+            "create": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            },
+            "push": {
+              "rules": {
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                },
+                "global:Project-Owners": {
+                  "action": "ALLOW"
+                }
+              }
+            }
+          }
+        },
+        "refs/*": {
+          "permissions": {
+            "read": {
+              "rules": {
+                "global:Anonymous-Users": {
+                  "action": "ALLOW"
+                },
+                "53a4f647a89ea57992571187d8025f830625192a": {
+                  "action": "ALLOW"
+                }
+              }
+            }
+          }
+        }
+      },
+      "is_owner": true,
+      "owner_of": [
+        "GLOBAL_CAPABILITIES",
+        "refs/meta/config",
+        "refs/for/refs/*",
+        "refs/tags/*",
+        "refs/heads/*",
+        "refs/*"
+      ],
+      "can_upload": true,
+      "can_add": true,
+      "config_visible": true
+    },
+    "MyProject": {
+      "revision": "61157ed63e14d261b6dca40650472a9b0bd88474",
+      "inherits_from": {
+        "kind": "gerritcodereview#project",
+        "id": "All-Projects",
+        "name": "All-Projects",
+        "description": "Access inherited by all other projects."
+      },
+      "local": {},
+      "is_owner": true,
+      "owner_of": [
+        "refs/*"
+      ],
+      "can_upload": true,
+      "can_add": true,
+      "config_visible": true
+    }
+  }
+----
+
+[[access-section-info]]
+AccessSectionInfo
+~~~~~~~~~~~~~~~~~
+The `AccessSectionInfo` describes the access rights that are assigned
+on a ref.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`permissions`        ||
+The permissions assigned on the ref of this access section as a map
+that maps the permission names to link:#permission-info[PermissionInfo]
+entities.
+|==================================
+
+[[permission-info]]
+PermissionInfo
+~~~~~~~~~~~~~~
+The `PermissionInfo` entity contains information about an assigned
+permission.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name     ||Description
+|`label`        |optional|
+The name of the label. Not set if it's not a label permission.
+|`exclusive`    |not set if `false`|
+Whether this permission is assigned exclusively.
+|`rules`        ||
+The rules assigned for this permission as a map that maps the UUIDs of
+the groups for which the permission are assigned to
+link:#permission-info[PermissionRuleInfo] entities.
+|==================================
+
+[[permission-rule-info]]
+PermissionRuleInfo
+~~~~~~~~~~~~~~~~~~
+The `PermissionRuleInfo` entity contains information about a permission
+rule that is assigned to group.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name     ||Description
+|`action`       ||
+The action of this rule. For normal permissions this can be `ALLOW`,
+`DENY` or `BLOCK`. Special values for global capabilities are
+`INTERACTIVE` and `BATCH`.
+|`force`        |not set if `false`|
+Whether the force flag is set.
+|`min`          |
+not set if range if empty (from `0` to `0`) or not set|
+The min value of the permission range.
+|`max`          |
+not set if range if empty (from `0` to `0`) or not set|
+The max value of the permission range.
+|==================================
+
+[[project-access-info]]
+ProjectAccessInfo
+~~~~~~~~~~~~~~~~~
+The `ProjectAccessInfo` entity contains information about the access
+rights for a project.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`revision`           ||
+The revision of the `refs/meta/config` branch from which the access
+rights were loaded.
+|`inherits_from`      |not set for the `All-Project` project|
+The parent project from which permissions are inherited as a
+link:rest-api-projects.html#project-info[ProjectInfo] entity.
+|`local`              ||
+The local access rights of the project as a map that maps the refs to
+link:#access-section-info[AccessSectionInfo] entities.
+|`is_owner`           |not set if `false`|
+Whether the calling user owns this project.
+|`owner_of`           ||The list of refs owned by the calling user.
+|`can_upload`         |not set if `false`|
+Whether the calling user can upload to any ref.
+|`can_add`            |not set if `false`|
+Whether the calling user can add any ref.
+|`config_visible`     |not set if `false`|
+Whether the calling user can see the `refs/meta/config` branch of the
+project.
+|==================================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index f17a741..70431c6 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -5,8 +5,9 @@
 Please also take note of the general information on the
 link:rest-api.html[REST API].
 
-Endpoints
----------
+[[account-endpoints]]
+Account Endpoints
+-----------------
 
 [[get-account]]
 Get Account
@@ -31,10 +32,565 @@
   {
     "_account_id": 1000096,
     "name": "John Doe",
+    "email": "john.doe@example.com",
+    "username": "john"
+  }
+----
+
+[[create-account]]
+Create Account
+~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#username[\{username\}]'
+
+Creates a new account.
+
+In the request body additional data for the account can be provided as
+link:#account-input[AccountInput].
+
+.Request
+----
+  PUT /accounts/john HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "name": "John Doe",
+    "email": "john.doe@example.com",
+    "ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw==",
+    "http_password": "19D9aIn7zePb",
+    "groups": [
+      "MyProject-Owners"
+    ]
+  }
+----
+
+As response a detailed link:#account-info[AccountInfo] entity is
+returned that describes the created account.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "_account_id": 1000195,
+    "name": "John Doe",
     "email": "john.doe@example.com"
   }
 ----
 
+[[get-account-name]]
+Get Account Name
+~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/name'
+
+Retrieves the full name of an account.
+
+.Request
+----
+  GET /accounts/self/name HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "John Doe"
+----
+
+If the account does not have a name an empty string is returned.
+
+[[set-account-name]]
+Set Account Name
+~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/name'
+
+Sets the full name of an account.
+
+The new account name must be provided in the request body inside
+a link:#account-name-input[AccountNameInput] entity.
+
+.Request
+----
+  PUT /accounts/self/name HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "name": "John F. Doe"
+  }
+----
+
+As response the new account name is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "John F. Doe"
+----
+
+If the name was deleted the response is "`204 No Content`".
+
+Some realms may not allow to modify the account name. In this case the
+request is rejected with "`405 Method Not Allowed`".
+
+[[delete-account-name]]
+Delete Account Name
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/name'
+
+Deletes the name of an account.
+
+.Request
+----
+  DELETE /accounts/self/name HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[get-username]]
+Get Username
+~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/username'
+
+Retrieves the username of an account.
+
+.Request
+----
+  GET /accounts/self/username HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "john.doe"
+----
+
+If the account does not have a username the response is `404 Not Found`.
+
+[[get-active]]
+Get Active
+~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/active'
+
+Checks if an account is active.
+
+.Request
+----
+  GET /accounts/john.doe@example.com/active HTTP/1.0
+----
+
+As response `200 OK` is returned for an active account and
+`404 Not Found` is returned for an inactive account.
+
+.Response
+----
+  HTTP/1.1 200 OK
+----
+
+[[set-active]]
+Set Active
+~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/active'
+
+Sets the account state to active.
+
+.Request
+----
+  PUT /accounts/john.doe@example.com/active HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 201 Created
+----
+
+If the account was already active the response is `200 OK`.
+
+[[delete-active]]
+Delete Active
+~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/active'
+
+Sets the account state to inactive.
+
+.Request
+----
+  DELETE /accounts/john.doe@example.com/active HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+If the account was already inactive the response is `404 Not Found`.
+
+[[get-http-password]]
+Get HTTP Password
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/password.http'
+
+Retrieves the HTTP password of an account.
+
+.Request
+----
+  GET /accounts/john.doe@example.com/password.http HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "ETxgpih8xrNs"
+----
+
+If the account does not have an HTTP password the response is `404 Not Found`.
+
+[[set-http-password]]
+Set/Generate HTTP Password
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/password.http'
+
+Sets/Generates the HTTP password of an account.
+
+The options for setting/generating the HTTP password must be provided
+in the request body inside a link:#http-password-input[
+HttpPasswordInput] entity.
+
+.Request
+----
+  PUT /accounts/self/password.http HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "generate": true
+  }
+----
+
+As response the new HTTP password is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "ETxgpih8xrNs"
+----
+
+If the HTTP password was deleted the response is "`204 No Content`".
+
+[[delete-http-password]]
+Delete HTTP Password
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/password.http'
+
+Deletes the HTTP password of an account.
+
+.Request
+----
+  DELETE /accounts/self/password.http HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[list-account-emails]]
+List Account Emails
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/emails'
+
+Returns the email addresses that are configured for the specified user.
+
+.Request
+----
+  GET /accounts/self/emails HTTP/1.0
+----
+
+As response the email addresses of the user are returned as a list of
+link:#email-info[EmailInfo] entities.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "email": "john.doe@example.com",
+      "preferred": true
+    },
+    {
+      "email": "j.doe@example.com"
+    }
+  ]
+----
+
+[[get-account-email]]
+Get Account Email
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]'
+
+Retrieves an email address of a user.
+
+.Request
+----
+  GET /accounts/self/emails/john.doe@example.com HTTP/1.0
+----
+
+As response an link:#email-info[EmailInfo] entity is returned that
+describes the email address.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "email": "john.doe@example.com",
+    "preferred": true
+  }
+----
+
+[[create-account-email]]
+Create Account Email
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]'
+
+Registers a new email address for the user. A verification email is
+sent with a link that needs to be visited to confirm the email address,
+unless `DEVELOPMENT_BECOME_ANY_ACCOUNT` is used as authentication type.
+For the development mode email addresses are directly added without
+confirmation. A Gerrit administrator may add an email address without
+confirmation by setting `no_confirmation` in the
+link:#email-input[EmailInput].
+
+In the request body additional data for the email address can be
+provided as link:#email-input[EmailInput].
+
+.Request
+----
+  PUT /accounts/self/emails/john.doe@example.com HTTP/1.0
+----
+
+As response the new email address is returned as
+link:#email-info[EmailInfo] entity.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "email": "john.doe@example.com",
+    "pending_confirmation": true
+  }
+----
+
+[[delete-account-email]]
+Delete Account Email
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]'
+
+Deletes an email address of an account.
+
+.Request
+----
+  DELETE /accounts/self/emails/john.doe@example.com HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[set-preferred-email]]
+Set Preferred Email
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]/preferred'
+
+Sets an email address as preferred email address for an account.
+
+.Request
+----
+  PUT /accounts/self/emails/john.doe@example.com/preferred HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 201 Created
+----
+
+If the email address was already the preferred email address of the
+account the response is "`200 OK`".
+
+[[list-ssh-keys]]
+List SSH Keys
+~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/sshkeys'
+
+Returns the SSH keys of an account.
+
+.Request
+----
+  GET /accounts/self/sshkeys HTTP/1.0
+----
+
+As response the SSH keys of the account are returned as a list of
+link:#ssh-key-info[SshKeyInfo] entities.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "seq": 1,
+      "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d john.doe@example.com",
+      "encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d",
+      "algorithm": "ssh-rsa",
+      "comment": "john.doe@example.com",
+      "valid": true
+    }
+  ]
+----
+
+[[get-ssh-key]]
+Get SSH Key
+~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/sshkeys/link:#ssh-key-id[\{ssh-key-id\}]'
+
+Retrieves an SSH key of a user.
+
+.Request
+----
+  GET /accounts/self/sshkeys/1 HTTP/1.0
+----
+
+As response an link:#ssh-key-info[SshKeyInfo] entity is returned that
+describes the SSH key.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "seq": 1,
+    "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d john.doe@example.com",
+    "encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d",
+    "algorithm": "ssh-rsa",
+    "comment": "john.doe@example.com",
+    "valid": true
+  }
+----
+
+[[add-ssh-key]]
+Add SSH Key
+~~~~~~~~~~~
+[verse]
+'POST /accounts/link:#account-id[\{account-id\}]/sshkeys'
+
+Adds an SSH key for a user.
+
+The SSH public key must be provided as raw content in the request body.
+
+.Request
+----
+  POST /accounts/self/sshkeys HTTP/1.0
+  Content-Type: plain/text
+
+  AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d
+----
+
+As response an link:#ssh-key-info[SshKeyInfo] entity is returned that
+describes the new SSH key.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "seq": 2,
+    "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d john.doe@example.com",
+    "encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d",
+    "algorithm": "ssh-rsa",
+    "comment": "john.doe@example.com",
+    "valid": true
+  }
+----
+
+[[delete-ssh-key]]
+Delete SSH Key
+~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/sshkeys/link:#ssh-key-id[\{ssh-key-id\}]'
+
+Deletes an SSH key of a user.
+
+.Request
+----
+  DELETE /accounts/self/sshkeys/2 HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[list-account-capabilities]]
 List Account Capabilities
 ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -103,8 +659,7 @@
     "flushCaches": true,
     "viewConnections": true,
     "viewQueue": true,
-    "runGC": true,
-    "startReplication": true
+    "runGC": true
   }
 ----
 
@@ -366,6 +921,87 @@
   }
 ----
 
+[[get-starred-changes]]
+Get Starred Changes
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/starred.changes'
+
+Gets the changes starred by the identified user account. This
+URL endpoint is functionally identical to the changes query
+`GET /changes/?q=is:starred`. The result is a list of
+link:rest-api-changes.html#change-info[ChangeInfo] entities.
+
+.Request
+----
+  GET /a/accounts/self/starred.changes
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#change",
+      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "project": "myProject",
+      "branch": "master",
+      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "subject": "Implementing Feature X",
+      "status": "NEW",
+      "created": "2013-02-01 09:59:32.126000000",
+      "updated": "2013-02-21 11:16:36.775000000",
+      "mergeable": true,
+      "_sortkey": "0023412400000f7d",
+      "_number": 3965,
+      "owner": {
+        "name": "John Doe"
+      }
+    }
+  ]
+----
+
+[[star-change]]
+Star Change
+~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/starred.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
+
+Star a change. Starred changes are returned for the search query
+`is:starred` or `starredby:USER` and automatically notify the user
+whenever updates are made to the change.
+
+.Request
+----
+  PUT /a/accounts/self/starred.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[unstar-change]]
+Unstar Change
+~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/starred.changes/link:rest-api-changes#change-id[\{change-id\}]'
+
+Unstar a change. Removes the starred flag, stopping notifications.
+
+.Request
+----
+  DELETE /a/accounts/self/starred.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
 
 [[ids]]
 IDs
@@ -391,6 +1027,22 @@
 Identifier of a global capability. Valid values are all field names of
 the link:#capability-info[CapabilityInfo] entity.
 
+[[email-id]]
+\{email-id\}
+~~~~~~~~~~~~
+An email address, or `preferred` for the preferred email address of the
+user.
+
+[[username]]
+\{username\}
+~~~~~~~~~~~~
+The user name.
+
+[[ssh-key-id]]
+\{ssh-key-id\}
+~~~~~~~~~~~~~~
+The sequence number of the SSH key.
+
 
 [[json-entities]]
 JSON Entities
@@ -406,12 +1058,50 @@
 |Field Name    ||Description
 |`_account_id` ||The numeric ID of the account.
 |`name`        |optional|The full name of the user. +
-Only set if detailed account information is requested.
+Only set if link:rest-api-changes.html#detailed-accounts[detailed
+account information] is requested.
 |`email`       |optional|
 The email address the user prefers to be contacted through. +
-Only set if detailed account information is requested.
+Only set if link:rest-api-changes.html#detailed-accounts[detailed
+account information] is requested.
+|`username`    |optional|The username of the user. +
+Only set if link:rest-api-changes.html#detailed-accounts[detailed
+account information] is requested.
 |===========================
 
+[[account-input]]
+AccountInput
+~~~~~~~~~~~~
+The `AccountInput` entity contains information for the creation of
+a new account.
+
+[options="header",width="50%",cols="1,^2,4"]
+|============================
+|Field Name     ||Description
+|`username`     |optional|
+The user name. If provided, must match the user name from the URL.
+|`name`         |optional|The full name of the user.
+|`email`        |optional|The email address of the user.
+|`ssh_key`      |optional|The public SSH key of the user.
+|`http_password`|optional|The HTTP password of the user.
+|`groups`       |optional|
+A list of link:rest-api-groups.html#group-id[group IDs] that identify
+the groups to which the user should be added.
+|============================
+
+[[account-name-input]]
+AccountNameInput
+~~~~~~~~~~~~~~~~
+The `AccountNameInput` entity contains information for setting a name
+for an account.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`name`     |optional|The new full name of the account. +
+If not set or if set to an empty string, the account name is deleted.
+|=============================
+
 [[capability-info]]
 CapabilityInfo
 ~~~~~~~~~~~~~~
@@ -453,9 +1143,6 @@
 |`runGC`  |not set if `false`|Whether the user has the
 link:access-control.html#capability_runGC[Run Garbage Collection]
 capability.
-|`startReplication`  |not set if `false`|Whether the user has the
-link:access-control.html#capability_startReplication[Start Replication]
-capability.
 |=================================
 
 [[diff-preferences-info]]
@@ -551,6 +1238,65 @@
 Number of spaces that should be used to display one tab.
 |=====================================
 
+[[email-info]]
+EmailInfo
+~~~~~~~~~
+The `EmailInfo` entity contains information about an email address of a
+user.
+
+[options="header",width="50%",cols="1,^1,5"]
+|========================
+|Field Name ||Description
+|`email`    ||The email address.
+|`preferred`|not set if `false`|
+Whether this is the preferred email address of the user.
+|`pending_confirmation`|not set if `false`|
+Set true if the user must confirm control of the email address
+by following a verification link before Gerrit will permit use of
+this address.
+|========================
+
+[[email-input]]
+EmailInput
+~~~~~~~~~~
+The `EmailInput` entity contains information for registering a new
+email address.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==============================
+|Field Name       ||Description
+|`email`          ||
+The email address. If provided, must match the email address from the
+URL.
+|`preferred`      |`false` if not set|
+Whether the new email address should become the preferred email address
+of the user (only supported if `no_confirmation` is set or if the
+authentication type is `DEVELOPMENT_BECOME_ANY_ACCOUNT`).
+|`no_confirmation`|`false` if not set|
+Whether the email address should be added without confirmation. In this
+case no verification email is sent to the user. +
+Only Gerrit administrators are allowed to add email addresses without
+confirmation.
+|==============================
+
+[[http-password-input]]
+HttpPasswordInput
+~~~~~~~~~~~~~~~~~
+The `HttpPasswordInput` entity contains information for setting/generating
+an HTTP password.
+
+[options="header",width="50%",cols="1,^1,5"]
+|============================
+|Field Name     ||Description
+|`generate`     |`false` if not set|
+Whether a new HTTP password should be generated
+|`http_password`|optional|
+The new HTTP password. Only Gerrit administrators may set the HTTP
+password directly. +
+If empty or not set and `generate` is false or not set, the HTTP
+password is deleted.
+|============================
+
 [[query-limit-info]]
 QueryLimitInfo
 ~~~~~~~~~~~~~~
@@ -564,6 +1310,23 @@
 |`max`               |Upper limit.
 |================================
 
+[[ssh-key-info]]
+SshKeyInfo
+~~~~~~~~~~
+The `SshKeyInfo` entity contains information about an SSH key of a
+user.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=============================
+|Field Name      ||Description
+|`seq`           ||The sequence number of the SSH key.
+|`ssh_public_key`||The complete public SSH key.
+|`encoded_key`   ||The encoded key.
+|`algorithm`     ||The algorithm of the SSH key.
+|`comment`       |optional|The comment of the SSH key.
+|`valid`         ||Whether the SSH key is valid.
+|=============================
+
 
 GERRIT
 ------
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 09c7e11..9404d00 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -48,7 +48,6 @@
       "status": "NEW",
       "created": "2012-07-17 07:18:30.854000000",
       "updated": "2012-07-17 07:19:27.766000000",
-      "reviewed": true,
       "mergeable": true,
       "_sortkey": "001e7057000006dc",
       "_number": 1756,
@@ -120,7 +119,6 @@
         "status": "NEW",
         "created": "2012-07-17 07:18:30.854000000",
         "updated": "2012-07-17 07:19:27.766000000",
-        "reviewed": true,
         "mergeable": true,
         "_sortkey": "001e7057000006dc",
         "_number": 1756,
@@ -173,11 +171,25 @@
 * `ALL_REVISIONS`: describe all revisions, not just current.
 --
 
+[[download_commands]]
+--
+* `DOWNLOAD_COMMANDS`: include the `commands` field in the
+  link:#fetch-info[FetchInfo] for revisions. Only valid when the
+  `CURRENT_REVISION` or `ALL_REVISIONS` option is selected.
+--
+
+[[draft_comments]]
+--
+* `DRAFT_COMMENTS`: include the `has_draft_comments` field for
+  revisions. Only valid when the `CURRENT_REVISION` or `ALL_REVISIONS`
+  option is selected.
+--
+
 [[current-commit]]
 --
 * `CURRENT_COMMIT`: parse and output all header fields from the
-  commit object, including message. Only valid when the current
-  revision or all revisions are selected.
+  commit object, including message. Only valid when the
+  `CURRENT_REVISION` or `ALL_REVISIONS` option is selected.
 --
 
 [[all-commits]]
@@ -191,21 +203,21 @@
 --
 * `CURRENT_FILES`: list files modified by the commit, including
   basic line counts inserted/deleted per file. Only valid when
-  the current revision or all revisions are selected.
+  the `CURRENT_REVISION` or `ALL_REVISIONS` option is selected.
 --
 
 [[all-files]]
 --
 * `ALL_FILES`: list files modified by the commit, including
   basic line counts inserted/deleted per file. If only the
-  `CURRENT_REVISION` was requested the only that commit's
+  `CURRENT_REVISION` was requested then only that commit's
   modified files will be output.
 --
 
 [[detailed-accounts]]
 --
-* `DETAILED_ACCOUNTS`: include `_account_id` and `email` fields when
-  referencing accounts.
+* `DETAILED_ACCOUNTS`: include `_account_id`, `email` and `username`
+  fields when referencing accounts.
 --
 
 [[messages]]
@@ -213,9 +225,22 @@
 * `MESSAGES`: include messages associated with the change.
 --
 
+[[actions]]
+--
+* `CURRENT_ACTIONS`: include information on available actions
+  for the change and its current revision. The caller must be
+  authenticated to obtain the available actions.
+--
+
+[[reviewed]]
+--
+* `REVIEWED`: include the `reviewed` field if the caller is
+  authenticated and has commented on the current revision.
+--
+
 .Request
 ----
-  GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
+  GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
 ----
 
 .Response
@@ -249,11 +274,33 @@
           "fetch": {
             "git": {
               "url": "git://localhost/gerrit",
-              "ref": "refs/changes/97/97/1"
+              "ref": "refs/changes/97/97/1",
+              "commands": {
+                "Checkout": "git fetch git://localhost/gerrit refs/changes/97/97/1 \u0026\u0026 git checkout FETCH_HEAD",
+                "Cherry-Pick": "git fetch git://localhost/gerrit refs/changes/97/97/1 \u0026\u0026 git cherry-pick FETCH_HEAD",
+                "Format-Patch": "git fetch git://localhost/gerrit refs/changes/97/97/1 \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+                "Pull": "git pull git://localhost/gerrit refs/changes/97/97/1"
+              }
             },
             "http": {
-              "url": "http://127.0.0.1:8080/gerrit",
-              "ref": "refs/changes/97/97/1"
+              "url": "http://myuser@127.0.0.1:8080/gerrit",
+              "ref": "refs/changes/97/97/1",
+              "commands": {
+                "Checkout": "git fetch http://myuser@127.0.0.1:8080/gerrit refs/changes/97/97/1 \u0026\u0026 git checkout FETCH_HEAD",
+                "Cherry-Pick": "git fetch http://myuser@127.0.0.1:8080/gerrit refs/changes/97/97/1 \u0026\u0026 git cherry-pick FETCH_HEAD",
+                "Format-Patch": "git fetch http://myuser@127.0.0.1:8080/gerrit refs/changes/97/97/1 \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+                "Pull": "git pull http://myuser@127.0.0.1:8080/gerrit refs/changes/97/97/1"
+              }
+            },
+            "ssh": {
+              "url": "ssh://myuser@*:29418/gerrit",
+              "ref": "refs/changes/97/97/1",
+              "commands": {
+                "Checkout": "git fetch ssh://myuser@*:29418/gerrit refs/changes/97/97/1 \u0026\u0026 git checkout FETCH_HEAD",
+                "Cherry-Pick": "git fetch ssh://myuser@*:29418/gerrit refs/changes/97/97/1 \u0026\u0026 git cherry-pick FETCH_HEAD",
+                "Format-Patch": "git fetch ssh://myuser@*:29418/gerrit refs/changes/97/97/1 \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+                "Pull": "git pull ssh://myuser@*:29418/gerrit refs/changes/97/97/1"
+              }
             }
           },
           "commit": {
@@ -319,6 +366,11 @@
 
 Retrieves a change.
 
+Additional fields can be obtained by adding `o` parameters, each
+option requires more database lookups and slows down the query
+response time to the client so they are generally disabled by
+default. Fields are described in link:#list-changes[Query Changes].
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
@@ -344,7 +396,6 @@
     "status": "NEW",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "reviewed": true,
     "mergeable": true,
     "_sortkey": "0023412400000f7d",
     "_number": 3965,
@@ -364,6 +415,11 @@
 detailed labels], link:#detailed-accounts[detailed accounts], and
 link:#messages[messages].
 
+Additional fields can be obtained by adding `o` parameters, each
+option requires more database lookups and slows down the query
+response time to the client so they are generally disabled by
+default. Fields are described in link:#list-changes[Query Changes].
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/detail HTTP/1.0
@@ -389,14 +445,14 @@
     "status": "NEW",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "reviewed": true,
     "mergeable": true,
     "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "_account_id": 1000096,
       "name": "John Doe",
-      "email": "john.doe@example.com"
+      "email": "john.doe@example.com",
+      "username": "jdoe"
     },
     "labels": {
       "Verified": {
@@ -405,13 +461,15 @@
             "value": 0,
             "_account_id": 1000096,
             "name": "John Doe",
-            "email": "john.doe@example.com"
+            "email": "john.doe@example.com",
+            "username": "jdoe"
           },
           {
             "value": 0,
             "_account_id": 1000097,
             "name": "Jane Roe",
-            "email": "jane.roe@example.com"
+            "email": "jane.roe@example.com",
+            "username": "jroe"
           }
         ],
         "values": {
@@ -424,25 +482,29 @@
         "recommended": {
           "_account_id": 1000097,
           "name": "Jane Roe",
-          "email": "jane.roe@example.com"
+          "email": "jane.roe@example.com",
+          "username": "jroe"
         },
         "disliked": {
           "_account_id": 1000096,
           "name": "John Doe",
-          "email": "john.doe@example.com"
+          "email": "john.doe@example.com",
+          "username": "jdoe"
         },
         "all": [
           {
             "value": -1,
             "_account_id": 1000096,
             "name": "John Doe",
-            "email": "john.doe@example.com"
+            "email": "john.doe@example.com",
+            "username": "jdoe"
           },
           {
             "value": 1,
             "_account_id": 1000097,
             "name": "Jane Roe",
-            "email": "jane.roe@example.com"
+            "email": "jane.roe@example.com",
+            "username": "jroe"
           }
         ]
         "values": {
@@ -472,12 +534,14 @@
       {
         "_account_id": 1000096,
         "name": "John Doe",
-        "email": "john.doe@example.com"
+        "email": "john.doe@example.com",
+        "username": "jdoe"
       },
       {
         "_account_id": 1000097,
         "name": "Jane Roe",
-        "email": "jane.roe@example.com"
+        "email": "jane.roe@example.com",
+        "username": "jroe"
       }
     ],
     "messages": [
@@ -486,7 +550,8 @@
         "author": {
           "_account_id": 1000096,
           "name": "John Doe",
-          "email": "john.doe@example.com"
+          "email": "john.doe@example.com",
+          "username": "jdoe"
         },
         "updated": "2013-03-23 21:34:02.419000000",
         "message": "Patch Set 1:\n\nThis is the first message.",
@@ -497,7 +562,8 @@
         "author": {
           "_account_id": 1000097,
           "name": "Jane Roe",
-          "email": "jane.roe@example.com"
+          "email": "jane.roe@example.com",
+          "username": "jroe"
         },
         "updated": "2013-03-23 21:36:52.332000000",
         "message": "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.",
@@ -628,7 +694,6 @@
     "status": "ABANDONED",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "reviewed": true,
     "mergeable": true,
     "_sortkey": "0023412400000f7d",
     "_number": 3965,
@@ -687,7 +752,6 @@
     "status": "NEW",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "reviewed": true,
     "mergeable": true,
     "_sortkey": "0023412400000f7d",
     "_number": 3965,
@@ -710,6 +774,95 @@
   change is new
 ----
 
+[[rebase-change]]
+Rebase Change
+~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/rebase'
+
+Rebases a change.
+
+.Request
+----
+  POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/rebase HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the rebased change. Information about the current patch set
+is included.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+    "subject": "Implement Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": false,
+    "_sortkey": "0024cf9a000012bf",
+    "_number": 4799,
+    "owner": {
+      "name": "John Doe"
+    },
+    "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
+    "revisions": {
+      "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+        "_number": 2,
+        "fetch": {
+          "http": {
+            "url": "http://gerrit:8080/myProject",
+            "ref": "refs/changes/99/4799/2"
+          }
+        },
+        "commit": {
+          "parents": [
+            {
+              "commit": "b4003890dadd406d80222bf1ad8aca09a4876b70",
+              "subject": "Implement Feature A"
+            }
+        ],
+        "author": {
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "date": "2013-05-07 15:21:27.000000000",
+          "tz": 120
+        },
+        "committer": {
+          "name": "Gerrit Code Review",
+          "email": "gerrit-server@example.com",
+          "date": "2013-05-07 15:35:43.000000000",
+          "tz": 120
+        },
+        "subject": "Implement Feature X",
+        "message": "Implement Feature X\n\nAdded feature X."
+      }
+    }
+  }
+----
+
+If the change cannot be rebased, e.g. due to conflicts, the response is
+"`409 Conflict`" and the error message is contained in the response
+body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  The change could not be rebased due to a path conflict during merge.
+----
+
 [[revert-change]]
 Revert Change
 ~~~~~~~~~~~~~
@@ -746,7 +899,6 @@
     "status": "NEW",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "reviewed": true,
     "mergeable": true,
     "_sortkey": "0023412400000f7d",
     "_number": 3965,
@@ -811,7 +963,6 @@
     "status": "MERGED",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "reviewed": true,
     "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
@@ -833,6 +984,74 @@
   blocked by Verified
 ----
 
+[[publish-draft-change]]
+Publish Draft Change
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/publish'
+
+Publishes a draft change.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/publish HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[delete-draft-change]]
+Delete Draft Change
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]'
+
+Deletes a draft change.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[get-included-in]]
+Get Included In
+~~~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/in'
+
+Retrieves the branches and tags in which a change is included. As result
+an link:#included-in-info[IncludedInInfo] entity is returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/in HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#includedininfo",
+    "branches": [
+      "master"
+    ],
+    "tags": []
+  }
+----
+
 [[reviewer-endpoints]]
 Reviewer Endpoints
 ------------------
@@ -883,6 +1102,48 @@
   ]
 ----
 
+[[suggest-reviewers]]
+Suggest Reviewers
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/suggest_reviewers?q=J&n=5'
+
+Suggest the reviewers for a given query `q` and result limit `n`. If result
+limit is not passed, then the default 10 is used.
+
+As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries is returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/suggest_reviewers?q=J HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#suggestedreviewer",
+      "account": {
+        "_account_id": 1000097,
+        "name": "Jane Roe",
+        "email": "jane.roe@example.com"
+      }
+    },
+    {
+      "kind": "gerritcodereview#suggestedreviewer",
+      "group": {
+        "id": "4fd581c0657268f2bdcc26699fbf9ddb76e3a279",
+        "name": "Joiner"
+      }
+    }
+  ]
+----
+
 [[get-reviewer]]
 Get Reviewer
 ~~~~~~~~~~~~
@@ -1031,6 +1292,55 @@
 Revision Endpoints
 ------------------
 
+[[get-commit]]
+Get Commit
+~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/commit'
+
+Retrieves a parsed commit of a revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/commit HTTP/1.0
+----
+
+As response a link:#commit-info[CommitInfo] entity is returned that
+describes the revision.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#commit",
+    "parents": [
+      {
+        "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+        "subject": "Migrate contributor agreements to All-Projects."
+      }
+    ],
+    "author": {
+      "name": "Shawn O. Pearce",
+      "email": "sop@google.com",
+      "date": "2012-04-24 18:08:08.000000000",
+      "tz": -420
+    },
+    "committer": {
+      "name": "Shawn O. Pearce",
+      "email": "sop@google.com",
+      "date": "2012-04-24 18:08:08.000000000",
+      "tz": -420
+    },
+    "subject": "Use an EventBus to manage star icons",
+    "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+  }
+----
+
+
 [[get-review]]
 Get Review
 ~~~~~~~~~~
@@ -1070,7 +1380,6 @@
     "status": "NEW",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "reviewed": true,
     "mergeable": true,
     "_sortkey": "0023412400000f7d",
     "_number": 3965,
@@ -1218,6 +1527,95 @@
   }
 ----
 
+[[rebase-revision]]
+Rebase Revision
+~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/rebase'
+
+Rebases a revision.
+
+.Request
+----
+  POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/rebase HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the rebased change. Information about the current patch set
+is included.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+    "subject": "Implement Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": false,
+    "_sortkey": "0024cf9a000012bf",
+    "_number": 4799,
+    "owner": {
+      "name": "John Doe"
+    },
+    "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
+    "revisions": {
+      "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+        "_number": 2,
+        "fetch": {
+          "http": {
+            "url": "http://gerrit:8080/myProject",
+            "ref": "refs/changes/99/4799/2"
+          }
+        },
+        "commit": {
+          "parents": [
+            {
+              "commit": "b4003890dadd406d80222bf1ad8aca09a4876b70",
+              "subject": "Implement Feature A"
+            }
+        ],
+        "author": {
+          "name": "John Doe",
+          "email": "john.doe@example.com",
+          "date": "2013-05-07 15:21:27.000000000",
+          "tz": 120
+        },
+        "committer": {
+          "name": "Gerrit Code Review",
+          "email": "gerrit-server@example.com",
+          "date": "2013-05-07 15:35:43.000000000",
+          "tz": 120
+        },
+        "subject": "Implement Feature X",
+        "message": "Implement Feature X\n\nAdded feature X."
+      }
+    }
+  }
+----
+
+If the revision cannot be rebased, e.g. due to conflicts, the response is
+"`409 Conflict`" and the error message is contained in the response
+body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  The change could not be rebased due to a path conflict during merge.
+----
+
 [[submit-revision]]
 Submit Revision
 ~~~~~~~~~~~~~~~
@@ -1268,6 +1666,106 @@
   "revision 674ac754f91e64a0efb8087e59a176484bd534d1 is not current revision"
 ----
 
+[[publish-draft-revision]]
+Publish Draft Revision
+~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/publish'
+
+Publishes a draft revision.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/publish HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[delete-draft-revision]]
+Delete Draft Revision
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]'
+
+Deletes a draft revision.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1 HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[get-patch]]
+Get Patch
+~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/patch'
+
+Gets the formatted patch for one revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/patch HTTP/1.0
+----
+
+The formatted patch is returned as text encoded inside base64:
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=ISO-8859-1
+  X-FYI-Content-Encoding: base64
+  X-FYI-Content-Type: application/mbox
+
+  RnJvbSA3ZGFkY2MxNTNmZGVhMTdhYTg0ZmYzMmE2ZTI0NWRiYjY...
+----
+
+Adding query parameter `zip` (for example `/changes/.../patch?zip`)
+returns the patch as a single file inside of a ZIP archive. Clients
+can expand the ZIP to obtain the plain text patch, avoiding the
+need for a base64 decoding step. This option implies `download`.
+
+Query parameter `download` (e.g. `/changes/.../patch?download`)
+will suggest the browser save the patch as `commitsha1.diff.base64`,
+for later processing by command line tools.
+
+[[get-mergeable]]
+Get Mergeable
+~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/mergeable'
+
+Gets the method the server will use to submit (merge) the change and
+an indicator if the change is currently mergeable.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/mergeable HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    submit_type: "MERGE_IF_NECESSARY",
+    mergeable: true,
+  }
+----
+
 [[get-submit-type]]
 Get Submit Type
 ~~~~~~~~~~~~~~~
@@ -1319,7 +1817,7 @@
   Content-Type: application/json;charset=UTF-8
 
   )]}'
-  "cherry_pick"
+  "CHERRY_PICK"
 ----
 
 [[test-submit-rule]]
@@ -1641,13 +2139,261 @@
   }
 ----
 
+[[list-files]]
+List Files
+~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/'
+
+Lists the files that were modified, added or deleted in a revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ HTTP/1.0
+----
+
+As result a map is returned that maps the file path to a list of
+link:#file-info[FileInfo] entries. The entries in the map are
+sorted by file path.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "/COMMIT_MSG": {
+      "status": "A",
+      "lines_inserted": 7
+    },
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
+      "lines_inserted": 5,
+      "lines_deleted": 3
+    }
+  }
+----
+
+The request parameter `reviewed` changes the response to return a list
+of the paths the caller has marked as reviewed.  Clients that also
+need the FileInfo should make two requests.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/?reviewed HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    "/COMMIT_MSG",
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+  ]
+----
+
+[[get-content]]
+Get Content
+~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/content'
+
+Gets the content of a file from a certain revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/content HTTP/1.0
+----
+
+The content is returned as base64 encoded string.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
+----
+
+[[get-diff]]
+Get Diff
+~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/diff'
+
+Gets the diff of a file from a certain revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff HTTP/1.0
+----
+
+As response a link:#diff-info[DiffInfo] entity is returned that describes the diff.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]
+  {
+    "meta_a": {
+      "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+      "content_type": "text/x-java-source"
+    },
+    "meta_b": {
+      "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+      "content_type": "text/x-java-source"
+    },
+    "change_type": "MODIFIED",
+    "diff_header": [
+      "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 59b7670..9faf81c 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"
+    ],
+    "content": [
+      {
+        "ab": [
+          "// 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."
+        ]
+      },
+      {
+        "b": [
+          "//",
+          "// Add some more lines in the header."
+        ]
+      },
+      {
+        "ab": [
+          "",
+          "package com.google.gerrit.server.project;",
+          "",
+          "import com.google.common.collect.Maps;",
+          ...
+        ]
+      }
+      ...
+    ]
+  }
+----
+
+If the `intraline` parameter is specified, intraline differences are included in the diff.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/b6b9c10649b9041884046119ab794374470a1b45/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff?intraline HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]
+  {
+    "meta_a": {
+      "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+      "content_type": "text/x-java-source"
+    },
+    "meta_b": {
+      "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+      "content_type": "text/x-java-source"
+    },
+    "change_type": "MODIFIED",
+    "diff_header": [
+      "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 59b7670..9faf81c 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"
+    ],
+    "content": [
+      ...
+      {
+        "a": [
+          "/** Manages access control for Git references (aka branches, tags). */"
+        ],
+        "b": [
+          "/** Manages access control for the Git references (aka branches, tags). */"
+        ],
+        "edit_a": [],
+        "edit_b": [
+          [
+            31,
+            4
+          ]
+        ]
+      }
+      ]
+    }
+----
+
+The `base` parameter can be specified to control the base patch set from which the diff should
+be generated.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/b6b9c10649b9041884046119ab794374470a1b45/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff?base=2 HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]
+  {
+    "meta_a": {
+      "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+      "content_type": "text/x-java-source"
+    },
+    "meta_b": {
+      "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+      "content_type": "text/x-java-source"
+    },
+    "change_type": "MODIFIED",
+    "content": [
+      {
+        "skip": 578
+      }
+    ]
+  }
+----
+
+The `ignore-whitespace` parameter can be specified to control how whitespace differences are
+reported in the result.  Valid values are `NONE`, `TRAILING`, `CHANGED` or `ALL`.
+
+The `context` parameter can be specified to control the number of lines of surrounding context
+in the diff.  Valid values are `ALL` or number of lines.
+
 [[set-reviewed]]
 Set Reviewed
 ~~~~~~~~~~~~
 [verse]
-'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#patch-id[\{patch-id\}]/reviewed'
+'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/reviewed'
 
-Marks a patch of a revision as reviewed by the calling user.
+Marks a file of a revision as reviewed by the calling user.
 
 .Request
 ----
@@ -1659,16 +2405,16 @@
   HTTP/1.1 201 Created
 ----
 
-If the patch was already marked as reviewed by the calling user the
+If the file was already marked as reviewed by the calling user the
 response is "`200 OK`".
 
 [[delete-reviewed]]
 Delete Reviewed
 ~~~~~~~~~~~~~~~
 [verse]
-'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#patch-id[\{patch-id\}]/reviewed'
+'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/reviewed'
 
-Deletes the reviewed flag of the calling user from a patch of a revision.
+Deletes the reviewed flag of the calling user from a file of a revision.
 
 .Request
 ----
@@ -1680,6 +2426,106 @@
   HTTP/1.1 204 No Content
 ----
 
+[[cherry-pick]]
+Cherry Pick Revision
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/cherrypick'
+
+Cherry picks a revision to a destination branch.
+
+The commit message and destination branch must be provided in the request body inside a
+link:#cherrypick-input[CherryPickInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/cherrypick HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "message" : "Implementing Feature X",
+    "destination" : "release-branch"
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the resulting cherry picked change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "project": "myProject",
+    "branch": "release-branch",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+[[message]]
+Edit Commit Message
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/message'
+
+Edit commit message.
+
+The commit message must be provided in the request body inside a
+link:#cherrypick-input[CherryPickInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/message HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "message" : "Reword Implementing Feature X",
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "project": "myProject",
+    "branch": "release-branch",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "subject": "Reword Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
 
 [[ids]]
 IDs
@@ -1715,10 +2561,10 @@
 ~~~~~~~~~~~~
 UUID of a draft comment.
 
-[[patch-id]]
-\{patch-id\}
+[[file-id]]
+\{file-id\}
 ~~~~~~~~~~~~
-The file path of the patch.
+The path of the file.
 
 [[revision-id]]
 \{revision-id\}
@@ -1733,7 +2579,6 @@
   change ("674ac754"), at least 4 digits are required
 * a legacy numeric patch number ("1" for first patch set of the change)
 
-
 [[json-entities]]
 JSON Entities
 -------------
@@ -1751,6 +2596,33 @@
 change.
 |===========================
 
+[[action-info]]
+ActionInfo
+~~~~~~~~~~
+The `ActionInfo` entity describes a REST API call the client can
+make to manipulate a resource. These are frequently implemented by
+plugins and may be discovered at runtime.
+
+[options="header",width="50%",cols="1,^1,5"]
+|====================================
+|Field Name             ||Description
+|`method`               |optional|
+HTTP method to use with the action. Most actions use `POST`, `PUT`
+or `DELETE` to cause state changes.
+|`label`                |optional|
+Short title to display to a user describing the action. In the
+Gerrit web interface the label is used as the text on the button
+presented in the UI.
+|`title`                |optional|
+Longer text to display describing the action. In a web UI this
+should be the title attribute of the element, displaying when
+the user hovers the mouse.
+|`enabled`              |optional|
+If true the action is permitted at this time and the caller is
+likely allowed to execute it. This may change if state is updated
+at the server or permissions are modified. Not present if false.
+|====================================
+
 [[add-reviewer-result]]
 AddReviewerResult
 ~~~~~~~~~~~~~~~~~
@@ -1788,8 +2660,22 @@
 The vote that the user has given for the label. If present and zero, the
 user is permitted to vote on the label. If absent, the user is not
 permitted to vote on that label.
+|`date`        |optional|
+The time and date describing when the approval was made.
 |===========================
 
+[[group-base-info]]
+GroupBaseInfo
+~~~~~~~~~~~~~
+The `GroupBaseInfo` entity contains base information about the group.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`id`          |The id of the group.
+|`name`        |The name of the group.
+|==========================
+
 [[change-info]]
 ChangeInfo
 ~~~~~~~~~~
@@ -1824,6 +2710,7 @@
 Whether the calling user has starred this change.
 |`reviewed`           |not set if `false`|
 Whether the change was reviewed by the calling user.
+Only set if link:#reviewed[reviewed] is requested.
 |`mergeable`          |optional|
 Whether the change is mergeable. +
 Not set for merged changes.
@@ -1832,6 +2719,10 @@
 |`owner`              ||
 The owner of the change as an link:rest-api-accounts.html#account-info[
 AccountInfo] entity.
+|`actions`            |optional|
+Actions the caller might be able to perform on this revision. The
+information is a map of view name to link:#action-info[ActionInfo]
+entities.
 |`labels`             |optional|
 The labels of the change as a map that maps the label names to
 link:#label-info[LabelInfo] entries. +
@@ -1846,7 +2737,7 @@
 link:rest-api-accounts.html#account-info[AccountInfo] entities. +
 Only set if link:#detailed-labels[detailed labels] are requested.
 |`messages`|optional|
-Messages associated with the change as a list of 
+Messages associated with the change as a list of
 link:#change-message-info[ChangeMessageInfo] entities. +
 Only set if link:#messages[messages] are requested.
 |`current_revision`   |optional|
@@ -1883,6 +2774,18 @@
 Which patchset (if any) generated this message.
 |==================================
 
+[[cherrypick-input]]
+CherryPickInput
+~~~~~~~~~~~~~~~
+The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
+
+[options="header",width="50%",cols="1,6"]
+|===========================
+|Field Name    |Description
+|`message`     |Commit message for the cherry-picked change
+|`destination` |Destination Branch
+|===========================
+
 [[comment-info]]
 CommentInfo
 ~~~~~~~~~~~
@@ -1902,7 +2805,11 @@
 If not set, the default is `REVISION`.
 |`line`        |optional|
 The number of the line for which the comment was done. +
-If not set, it's a file comment.
+If range is set, this equals the end line of the range. +
+If neither line nor range is set, it's a file comment.
+|`range`       |optional|
+The range of the comment as a link:rest-api.html#comment-range[CommentRange]
+entity.
 |`in_reply_to` |optional|
 The URL encoded UUID of the comment to which this comment is a reply.
 |`message`     |optional|The comment message.
@@ -1910,7 +2817,7 @@
 The link:rest-api.html#timestamp[timestamp] of when this comment was
 written.
 |`author`      |optional|
-The author of the message as an +
+The author of the message as an
 link:rest-api-accounts.html#account-info[AccountInfo] entity. +
 Unset for draft comments, assumed to be the calling user.
 |===========================
@@ -1940,7 +2847,11 @@
 |`line`        |optional|
 The number of the line for which the comment should be added. +
 `0` if it is a file comment. +
-If not set, a file comment is added.
+If neither line nor range is set, a file comment is added. +
+If range is set, this should equal the end line of the range.
+|`range`       |optional|
+The range of the comment as a link:rest-api.html#comment-range[CommentRange]
+entity.
 |`in_reply_to` |optional|
 The URL encoded UUID of the comment to which this comment is a reply.
 |`updated`     |optional|
@@ -1952,6 +2863,20 @@
 comment is deleted.
 |===========================
 
+[[comment-range]]
+CommentRange
+~~~~~~~~~~~~
+The `CommentRange` entity describes the range of an inline comment.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`start_line`        ||The start line number of the range.
+|`start_character`   ||The character position in the start line.
+|`end_line`          ||The end line number of the range.
+|`end_character`     ||The character position in the end line.
+|===========================
+
 [[commit-info]]
 CommitInfo
 ~~~~~~~~~~
@@ -1963,7 +2888,8 @@
 |`commit`      |The commit ID.
 |`parent`      |
 The parent commits of this commit as a list of
-link:#commit-info[CommitInfo] entities.
+link:#commit-info[CommitInfo] entities. In parent
+only `commit` and `subject` fields are populated.
 |`author`      |The author of the commit as a
 link:#git-person-info[GitPersonInfo] entity.
 |`committer`   |The committer of the commit as a
@@ -1973,17 +2899,94 @@
 |`message`     |The commit message.
 |==========================
 
+[[diff-content]]
+DiffContent
+~~~~~~~~~~~
+The `DiffContent` entity contains information about the content differences
+in a file.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==========================
+|Field Name ||Description
+|`a`        |optional|Content only in the file on side A (deleted in B).
+|`b`        |optional|Content only in the file on side B (added in B).
+|`ab`       |optional|Content in the file on both sides (unchanged).
+|`edit_a`   |only present during a replace, i.e. both `a` and `b` are present|
+Text sections deleted from side A as a
+link:#diff-intraline-info[DiffIntralineInfo] entity.
+|`edit_b`   |only present during a replace, i.e. both `a` and `b` are present|
+Text sections inserted in side B as a
+link:#diff-intraline-info[DiffIntralineInfo] entity.
+|`skip`     |optional|count of lines skipped on both sides when the file is
+too large to include all common lines.
+|==========================
+
+[[diff-file-meta-info]]
+DiffFileMetaInfo
+~~~~~~~~~~~~~~~~
+The `DiffFileMetaInfo` entity contains meta information about a file diff.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`name`        |The name of the file.
+|`content_type`|The content type of the file.
+|==========================
+
+[[diff-info]]
+DiffInfo
+~~~~~~~~
+The `DiffInfo` entity contains information about the diff of a file
+in a revision.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==========================
+|Field Name        ||Description
+|`meta_a`          |not present when the file is added|
+Meta information about the file on side A as a
+link:#diff-file-meta-info[DiffFileMetaInfo] entity.
+|`meta_b`          |not present when the file is deleted|
+Meta information about the file on side B as a
+link:#diff-file-meta-info[DiffFileMetaInfo] entity.
+|`change_type`     ||The type of change (`ADDED`, `MODIFIED`, `DELETED`, `RENAMED`
+`COPIED`, `REWRITE`).
+|`intraline_status`|only set when the `intraline` parameter was specified in the request|
+Intraline status (`OK`, `ERROR`, `TIMEOUT`).
+|`diff_header`     ||A list of strings representing the patch set diff header.
+|`content`         ||The content differences in the file as a list of
+link:#diff-content[DiffContent] entities.
+|==========================
+
+[[diff-intraline-info]]
+DiffIntralineInfo
+~~~~~~~~~~~~~~~~~
+The `DiffIntralineInfo` entity contains information about intraline edits in a
+file.
+
+The information consists of a list of `<skip length, mark length>` pairs, where
+the skip length is the number of characters between the end of the previous edit
+and the start of this edit, and the mark length is the number of edited characters
+following the skip. The start of the edits is from the beginning of the related
+diff content lines.
+
+Note that the implied newline character at the end of each line is included in
+the length calculation, and thus it is possible for the edits to span newlines.
+
 [[fetch-info]]
 FetchInfo
 ~~~~~~~~~
 The `FetchInfo` entity contains information about how to fetch a patch
 set via a certain protocol.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",width="50%",cols="1,^1,5"]
 |==========================
-|Field Name    |Description
-|`url`         |The URL of the project.
-|`ref`         |The ref of the patch set.
+|Field Name    ||Description
+|`url`         ||The URL of the project.
+|`ref`         ||The ref of the patch set.
+|`commands`    |optional|
+The download commands for this patch set as a map that maps the command
+names to the commands. +
+Only set if link:#download_commands[download commands] are requested.
 |==========================
 
 [[file-info]]
@@ -2030,40 +3033,62 @@
 [[label-info]]
 LabelInfo
 ~~~~~~~~~
-The `LabelInfo` entity contains information about a label on a change.
+The `LabelInfo` entity contains information about a label on a change, always
+corresponding to the current patch set.
 
+There are two options that control the contents of `LabelInfo`:
+link:#labels[`LABELS`] and link:#detailed-labels[`DETAILED_LABELS`].
+
+* For a quick summary of the state of labels, use `LABELS`.
+* For detailed information about labels, including exact numeric votes for all
+  users and the allowed range of votes for the current user, use `DETAILED_LABELS`.
+
+Common fields
+^^^^^^^^^^^^^
 [options="header",width="50%",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
-|`approved`    |optional|The user who approved this label on the change
-as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`rejected`    |optional|The user who rejected this label on the change
-as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`recommended` |optional|The user who recommended this label on the
-change as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`disliked`    |optional|The user who disliked this label on the change
-as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`value`       |optional|The voting value of the user who
-recommended/disliked this label on the change if it is not
-"`+1`"/"`-1`". +
-Only set if link:#labels[labels] are requested.
 |`optional`    |not set if `false`|
 Whether the label is optional. Optional means the label may be set, but
 it's neither necessary for submission nor does it block submission if
 set.
+|===========================
+
+Fields set by `LABELS`
+^^^^^^^^^^^^^^^^^^^^^^
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`approved`    |optional|One user who approved this label on the change
+(voted the maximum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`rejected`    |optional|One user who rejected this label on the change
+(voted the minimum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`recommended` |optional|One user who recommended this label on the
+change (voted positively, but not the maximum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`disliked`    |optional|One user who disliked this label on the change
+(voted negatively, but not the minimum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`value`       |optional|The voting value of the user who
+recommended/disliked this label on the change if it is not
+"`+1`"/"`-1`".
+|===========================
+
+Fields set by `DETAILED_LABELS`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
 |`all`         |optional|List of all approvals for this label as a list
-of link:#approval-info[ApprovalInfo] entities. +
-Only set if link:#detailed-labels[detailed labels] are requested.
+of link:#approval-info[ApprovalInfo] entities.
 |`values`      |optional|A map of all values that are allowed for this
 label. The map maps the values ("`-2`", "`-1`", " `0`", "`+1`", "`+2`")
-to the value descriptions. +
-Only set if link:#detailed-labels[detailed labels] are requested.
+to the value descriptions.
 |===========================
 
+
 [[restore-input]]
 RestoreInput
 ~~~~~~~~~~~~
@@ -2138,6 +3163,10 @@
 after the review is stored. +
 Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
 If not set, the default is `ALL`.
+|`on_behalf_of`|optional|
+link:rest-api-accounts.html#account-id[\{account-id\}] the review
+should be posted on behalf of. To use this option the caller must
+have been granted `labelAs-NAME` permission for all keys of labels.
 |============================
 
 [[reviewer-info]]
@@ -2191,16 +3220,23 @@
 |===========================
 |Field Name    ||Description
 |`draft`       |not set if `false`|Whether the patch set is a draft.
+|`has_draft_comments`       |not set if `false`|Whether the patch
+set has one or more draft comments by the calling user. Only set if
+link:#draft_comments[draft comments] is requested.
 |`_number`     ||The patch set number.
 |`fetch`       ||
 Information about how to fetch this patch set. The fetch information is
 provided as a map that maps the protocol name ("`git`", "`http`",
 "`ssh`") to link:#fetch-info[FetchInfo] entities.
-|`commit`      ||The commit of the patch set as
+|`commit`      |optional|The commit of the patch set as
 link:#commit-info[CommitInfo] entity.
-|`files`       ||
+|`files`       |optional|
 The files of the patch set as a map that maps the file names to
 link:#file-info[FileInfo] entities.
+|`actions`     |optional|
+Actions the caller might be able to perform on this revision. The
+information is a map of view name to link:#action-info[ActionInfo]
+entities.
 |===========================
 
 [[rule-input]]
@@ -2221,6 +3257,17 @@
 to return results from the input rule.
 |===========================
 
+[[suggested-reviewer-info]]
+SuggestedReviewerInfo
+~~~~~~~~~~~~~~~~~~~~~
+The `SuggestedReviewerInfo` entity contains information about a reviewer
+that can be added to a change (an account or a group).
+
+`SuggestedReviewerInfo` has either the `account` field that contains
+the link:rest-api-accounts.html#account-info[AccountInfo] entity, or
+the `group` field that contains the
+link:rest-api-changes.html#group-base-info[GroupBaseInfo] entity.
+
 [[submit-info]]
 SubmitInfo
 ~~~~~~~~~~
@@ -2304,6 +3351,22 @@
 topic.
 |===========================
 
+[[included-in-info]]
+IncludedInInfo
+~~~~~~~~~~~~~~
+The `IncludedInInfo` entity contains information about the branches a
+change was merged into and tags it was tagged with.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name |Description
+|`kind`     |`gerritcodereview#includedininfo`
+|`branches` | The list of branches this change was merged into.
+Each branch is listed without the 'refs/head/' prefix.
+|`tags`     | The list of tags this change was tagged with.
+Each tag is listed without the 'refs/tags/' prefix.
+|==========================
+
 
 GERRIT
 ------
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
new file mode 100644
index 0000000..5bd5e0c
--- /dev/null
+++ b/Documentation/rest-api-config.txt
@@ -0,0 +1,222 @@
+Gerrit Code Review - /config/ REST API
+======================================
+
+This page describes the config related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[config-endpoints]]
+Config Endpoints
+---------------
+
+[[get-version]]
+Get Version
+~~~~~~~~~~~
+[verse]
+'GET /config/server/version'
+
+Returns the version of the Gerrit server.
+
+.Request
+----
+  GET /config/server/version HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "2.7"
+----
+
+[[list-capabilities]]
+List Capabilities
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /config/server/capabilities'
+
+Lists the capabilities that are available in the system. There are two
+kinds of capabilities: core and plugin-owned capabilities.
+
+As result a map of link:#capability-info[CapabilityInfo] entities is
+returned.
+
+The entries in the map are sorted by capability ID.
+
+.Request
+----
+  GET /config/server/capabilities/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "accessDatabase": {
+      "kind": "gerritcodereview#capability",
+      "id": "accessDatabase",
+      "name": "Access Database"
+    },
+    "administrateServer": {
+      "kind": "gerritcodereview#capability",
+      "id": "administrateServer",
+      "name": "Administrate Server"
+    },
+    "createAccount": {
+      "kind": "gerritcodereview#capability",
+      "id": "createAccount",
+      "name": "Create Account"
+    },
+    "createGroup": {
+      "kind": "gerritcodereview#capability",
+      "id": "createGroup",
+      "name": "Create Group"
+    },
+    "createProject": {
+      "kind": "gerritcodereview#capability",
+      "id": "createProject",
+      "name": "Create Project"
+    },
+    "emailReviewers": {
+      "kind": "gerritcodereview#capability",
+      "id": "emailReviewers",
+      "name": "Email Reviewers"
+    },
+    "flushCaches": {
+      "kind": "gerritcodereview#capability",
+      "id": "flushCaches",
+      "name": "Flush Caches"
+    },
+    "killTask": {
+      "kind": "gerritcodereview#capability",
+      "id": "killTask",
+      "name": "Kill Task"
+    },
+    "priority": {
+      "kind": "gerritcodereview#capability",
+      "id": "priority",
+      "name": "Priority"
+    },
+    "queryLimit": {
+      "kind": "gerritcodereview#capability",
+      "id": "queryLimit",
+      "name": "Query Limit"
+    },
+    "runGC": {
+      "kind": "gerritcodereview#capability",
+      "id": "runGC",
+      "name": "Run Garbage Collection"
+    },
+    "streamEvents": {
+      "kind": "gerritcodereview#capability",
+      "id": "streamEvents",
+      "name": "Stream Events"
+    },
+    "viewCaches": {
+      "kind": "gerritcodereview#capability",
+      "id": "viewCaches",
+      "name": "View Caches"
+    },
+    "viewConnections": {
+      "kind": "gerritcodereview#capability",
+      "id": "viewConnections",
+      "name": "View Connections"
+    },
+    "viewQueue": {
+      "kind": "gerritcodereview#capability",
+      "id": "viewQueue",
+      "name": "View Queue"
+    }
+  }
+----
+
+[[get-top-menus]]
+Get Top Menus
+~~~~~~~~~~~~~
+[verse]
+'GET /config/server/top-menus'
+
+Returns the list of additional top menu entries.
+
+.Request
+----
+  GET /config/server/top-menus HTTP/1.0
+----
+
+As response a list of the additional top menu entries as
+link:#top-menu-entry-info[TopMenuEntryInfo] entities is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "name": "Top Menu Entry",
+      "items": [
+        {
+          "url": "http://gerrit.googlecode.com/",
+          "name": "Gerrit",
+          "target": "_blank"
+        }
+      ]
+    }
+  ]
+----
+
+
+[[json-entities]]
+JSON Entities
+-------------
+
+[[capability-info]]
+CapabilityInfo
+~~~~~~~~~~~~~~
+The `CapabilityInfo` entity contains information about a capability.
+
+[options="header",width="50%",cols="1,6"]
+|=================================
+|Field Name           |Description
+|`kind`               |`gerritcodereview#capability`
+|`id`                 |capability ID
+|`name`               |capability name
+|=================================
+
+[[top-menu-entry-info]]
+TopMenuEntryInfo
+~~~~~~~~~~~~~~~~
+The `TopMenuEntryInfo` entity contains information about a top menu
+entry.
+
+[options="header",width="50%",cols="1,6"]
+|=================================
+|Field Name           |Description
+|`name`               |Name of the top menu entry.
+|`items`              |List of link:#top-menu-item-info[menu items].
+|=================================
+
+[[top-menu-item-info]]
+TopMenuItemInfo
+~~~~~~~~~~~~~~~
+The `TopMenuItemInfo` entity contains information about a menu item in
+a top menu entry.
+
+[options="header",width="50%",cols="1,^1,5"]
+|========================
+|Field Name ||Description
+|`url`      ||The URL of the menu item link.
+|`name`     ||The name of the menu item.
+|`target`   ||Target attribute of the menu item link.
+|`id`       |optional|The `id` attribute of the menu item link.
+|========================
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index e800d56..6289e5a 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -293,12 +293,14 @@
       {
         "_account_id": 1000097,
         "name": "Jane Roe",
-        "email": "jane.roe@example.com"
+        "email": "jane.roe@example.com",
+        "username": "jane"
       },
       {
         "_account_id": 1000096,
         "name": "John Doe",
         "email": "john.doe@example.com"
+        "username": "john"
       }
     ],
     "includes": []
@@ -620,12 +622,14 @@
     {
       "_account_id": 1000097,
       "name": "Jane Roe",
-      "email": "jane.roe@example.com"
+      "email": "jane.roe@example.com",
+      "username": "jane"
     },
     {
       "_account_id": 1000096,
       "name": "John Doe",
-      "email": "john.doe@example.com"
+      "email": "john.doe@example.com",
+      "username": "john"
     }
   ]
 ----
@@ -657,17 +661,20 @@
     {
       "_account_id": 1000097,
       "name": "Jane Roe",
-      "email": "jane.roe@example.com"
+      "email": "jane.roe@example.com",
+      "username": "jane"
     },
     {
       "_account_id": 1000096,
       "name": "John Doe",
-      "email": "john.doe@example.com"
+      "email": "john.doe@example.com",
+      "username": "john"
     },
     {
       "_account_id": 1000098,
       "name": "Richard Roe",
-      "email": "richard.roe@example.com"
+      "email": "richard.roe@example.com",
+      "username": "rroe"
     }
   ]
 ----
@@ -698,7 +705,8 @@
   {
     "_account_id": 1000096,
     "name": "John Doe",
-    "email": "john.doe@example.com"
+    "email": "john.doe@example.com",
+    "username": "john"
   }
 ----
 
@@ -728,7 +736,8 @@
   {
     "_account_id": 1000037,
     "name": "John Doe",
-    "email": "john.doe@example.com"
+    "email": "john.doe@example.com",
+    "username": "john"
   }
 ----
 
@@ -756,10 +765,10 @@
   Content-Type: application/json;charset=UTF-8
 
   {
-    "members": {
+    "members": [
       "jane.roe@example.com",
       "john.doe@example.com"
-    }
+    ]
   }
 ----
 
@@ -782,12 +791,14 @@
     {
       "_account_id": 1000057,
       "name": "Jane Roe",
-      "email": "jane.roe@example.com"
+      "email": "jane.roe@example.com",
+      "username": "jane"
     },
     {
       "_account_id": 1000037,
       "name": "John Doe",
-      "email": "john.doe@example.com"
+      "email": "john.doe@example.com",
+      "username": "john"
     }
   ]
 ----
@@ -827,10 +838,10 @@
   Content-Type: application/json;charset=UTF-8
 
   {
-    "members": {
+    "members": [
       "jane.roe@example.com",
       "john.doe@example.com"
-    }
+    ]
   }
 ----
 
@@ -923,7 +934,8 @@
 [verse]
 'PUT /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
 
-Includes a group into a Gerrit internal group.
+Includes an internal or external group into a Gerrit internal group.
+External groups must be specified using the UUID.
 
 .Request
 ----
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
new file mode 100644
index 0000000..2cc80f0
--- /dev/null
+++ b/Documentation/rest-api-plugins.txt
@@ -0,0 +1,279 @@
+Gerrit Code Review - /plugins/ REST API
+=======================================
+
+This page describes the plugin related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[plugin-endpoints]]
+Plugin Endpoints
+----------------
+
+Gerrit REST endpoints for installed plugins are available under
+'/plugins/link:#plugin-id[\{plugin-id\}]/gerrit~<endpoint-id>'.
+The `gerrit~` prefix ensures that the Gerrit REST endpoints for plugins
+do not clash with any REST endpoint that a plugin may offer under its
+namespace.
+
+
+[[list-plugins]]
+List Plugins
+~~~~~~~~~~~~
+[verse]
+'GET /plugins/'
+
+Lists the plugins installed on the Gerrit server. Only the enabled
+plugins are returned unless the `all` option is specified.
+
+As result a map is returned that maps the plugin IDs to
+link:#plugin-info[PluginInfo] entries. The entries in the map are sorted
+by plugin ID.
+
+.Request
+----
+  GET /plugins/?all HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "delete-project": {
+      "kind": "gerritcodereview#plugin",
+      "id": "delete-project",
+      "version": "2.8-SNAPSHOT"
+    },
+    "reviewers-by-blame": {
+      "kind": "gerritcodereview#plugin",
+      "id": "reviewers-by-blame",
+      "version": "2.8-SNAPSHOT",
+      "disabled": true
+    }
+  }
+----
+
+[[install-plugin]]
+Install Plugin
+~~~~~~~~~~~~~~
+[verse]
+'PUT /plugins/link:#plugin-id[\{plugin-id\}]'
+
+Installs a new plugin on the Gerrit server. If a plugin with the
+specified name already exists it is overwritten. Note: if the plugin
+provides its own name in the MANIFEST file, then the plugin name from
+the MANIFEST file has precedence over the \{plugin-id\} above.
+
+The plugin jar can either be sent as binary data in the request body
+or a URL to the plugin jar must be provided in the request body inside
+a link:#plugin-input[PluginInput] entity.
+
+.Request
+----
+  PUT /plugins/delete-project HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "url": "file:///gerrit/plugins/delete-project/delete-project-2.8.jar"
+  }
+----
+
+To provide the plugin jar as binary data in the request body the
+following curl command can be used:
+
+----
+  curl --digest --user admin:TNNuLkWsIV8w -X PUT --data-binary @delete-project-2.8.jar 'http://gerrit:8080/a/plugins/delete-project'
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#plugin",
+    "id": "delete-project",
+    "version": "2.8"
+  }
+----
+
+If an existing plugin was overwritten the response is "`200 OK`".
+
+[[get-plugin-status]]
+Get Plugin Status
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~status'
+
+Retrieves the status of a plugin on the Gerrit server.
+
+.Request
+----
+  GET /plugins/delete-project/gerrit~status HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#plugin",
+    "id": "delete-project",
+    "version": "2.8"
+  }
+----
+
+[[enable-plugin]]
+Enable Plugin
+~~~~~~~~~~~~~
+[verse]
+'POST /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~enable'
+
+Enables a plugin on the Gerrit server.
+
+.Request
+----
+  POST /plugins/delete-project/gerrit~enable HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#plugin",
+    "id": "delete-project",
+    "version": "2.8"
+  }
+----
+
+[[disable-plugin]]
+Disable Plugin
+~~~~~~~~~~~~~~
+[verse]
+'POST /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~disable'
+
+OR
+
+[verse]
+'DELETE /plugins/link:#plugin-id[\{plugin-id\}]'
+
+Disables a plugin on the Gerrit server.
+
+.Request
+----
+  POST /plugins/delete-project/gerrit~disable HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#plugin",
+    "id": "delete-project",
+    "version": "2.8",
+    "disabled": true
+  }
+----
+
+[[reload-plugin]]
+Reload Plugin
+~~~~~~~~~~~~~
+[verse]
+'POST /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~reload'
+
+Reloads a plugin on the Gerrit server.
+
+.Request
+----
+  POST /plugins/delete-project/gerrit~reload HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#plugin",
+    "id": "delete-project",
+    "version": "2.8",
+    "disabled": true
+  }
+----
+
+
+[[ids]]
+IDs
+---
+
+[[plugin-id]]
+\{plugin-id\}
+~~~~~~~~~~~~~
+The ID of the plugin.
+
+[[json-entities]]
+JSON Entities
+-------------
+
+[[plugin-info]]
+PluginInfo
+~~~~~~~~~~
+The `PluginInfo` entity describes a plugin.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=======================
+|Field Name||Description
+|`kind`    ||`gerritcodereview#plugin`
+|`id`      ||The ID of the plugin.
+|`version` ||The version of the plugin.
+|`disabled`|not set if `false`|Whether the plugin is disabled.
+|=======================
+
+[[plugin-input]]
+PluginInput
+~~~~~~~~~~~
+The `PluginInput` entity describes a plugin that should be installed.
+
+[options="header",width="50%",cols="1,6"]
+|======================
+|Field Name|Description
+|`url`     |URL to the plugin jar.
+|======================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 35caac4..35db183 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -5,9 +5,6 @@
 Please also take note of the general information on the
 link:rest-api.html[REST API].
 
-Endpoints
----------
-
 [[project-endpoints]]
 Project Endpoints
 -----------------
@@ -436,6 +433,7 @@
   )]}'
   {
     "kind": "gerritcodereview#project_config",
+    "description": "demo project",
     "use_contributor_agreements": {
       "value": true,
       "configured_value": "TRUE",
@@ -455,7 +453,93 @@
       "value": false,
       "configured_value": "FALSE",
       "inherited_value": true
+    },
+    "max_object_size_limit": {
+      "value": "15m",
+      "configured_value": "15m",
+      "inherited_value": "20m"
+    },
+    "submit_type": "MERGE_IF_NECESSARY",
+    "state": "ACTIVE",
+    "commentlinks": {},
+    "actions": {
+      "cookbook~hello-project": {
+        "method": "POST",
+        "label": "Say hello",
+        "title": "Say hello in different languages",
+        "enabled": true
+      }
     }
+  }
+----
+
+[[set-config]]
+Set Config
+~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/config'
+
+Sets the configuration of a project.
+
+The new configuration must be provided in the request body as a
+link:#config-input[ConfigInput] entity.
+
+.Request
+----
+  PUT /projects/myproject/config HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "description": "demo project",
+    "use_contributor_agreements": "FALSE",
+    "use_content_merge": "INHERIT",
+    "use_signed_off_by": "INHERIT",
+    "require_change_id": "TRUE",
+    "max_object_size_limit": "10m",
+    "submit_type": "REBASE_IF_NECESSARY",
+    "state": "ACTIVE"
+  }
+----
+
+As response the new configuration is returned as a link:#config-info[
+ConfigInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#project_config",
+    "use_contributor_agreements": {
+      "value": false,
+      "configured_value": "FALSE",
+      "inherited_value": false
+    },
+    "use_content_merge": {
+      "value": true,
+      "configured_value": "INHERIT",
+      "inherited_value": true
+    },
+    "use_signed_off_by": {
+      "value": false,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
+    "require_change_id": {
+      "value": true,
+      "configured_value": "TRUE",
+      "inherited_value": true
+    },
+    "max_object_size_limit": {
+      "value": "10m",
+      "configured_value": "10m",
+      "inherited_value": "20m"
+    },
+    "submit_type": "REBASE_IF_NECESSARY",
+    "state": "ACTIVE",
     "commentlinks": {}
   }
 ----
@@ -499,6 +583,281 @@
   done.
 ----
 
+[[branch-endpoints]]
+Branch Endpoints
+----------------
+
+[[list-branches]]
+List Branches
+~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/branches/'
+
+List the branches of a project.
+
+As result a list of link:#branch-info[BranchInfo] entries is
+returned.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/branches/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "HEAD",
+      "revision": "master"
+    },
+    {
+      "ref": "refs/meta/config",
+      "revision": "76016386a0d8ecc7b6be212424978bb45959d668"
+    },
+    {
+      "ref": "refs/heads/master",
+      "revision": "67ebf73496383c6777035e374d2d664009e2aa5c"
+    },
+    {
+      "ref": "refs/heads/stable",
+      "revision": "64ca533bd0eb5252d2fee83f63da67caae9b4674",
+      "can_delete": true
+    }
+  ]
+----
+
+[[get-branch]]
+Get Branch
+~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]'
+
+Retrieves a branch of a project.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/branches/master HTTP/1.0
+----
+
+As response a link:#branch-info[BranchInfo] entity is returned that
+describes the branch.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "ref": "refs/heads/master",
+    "revision": "67ebf73496383c6777035e374d2d664009e2aa5c"
+  }
+----
+
+[[create-branch]]
+Create Branch
+~~~~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]'
+
+Creates a new branch.
+
+In the request body additional data for the branch can be provided as
+link:#branch-input[BranchInput].
+
+.Request
+----
+  PUT /projects/MyProject/branches/stable HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "revision": "76016386a0d8ecc7b6be212424978bb45959d668"
+  }
+----
+
+As response a link:#branch-info[BranchInfo] entity is returned that
+describes the created branch.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "ref": "refs/heads/stable",
+    "revision": "76016386a0d8ecc7b6be212424978bb45959d668",
+    "can_delete": true
+  }
+----
+
+[[delete-branch]]
+Delete Branch
+~~~~~~~~~~~~~
+[verse]
+'DELETE /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]'
+
+Deletes a branch.
+
+.Request
+----
+  DELETE /projects/MyProject/branches/stable HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[child-project-endpoints]]
+Child Project Endpoints
+-----------------------
+
+[[list-child-projects]]
+List Child Projects
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/children/'
+
+List the direct child projects of a project.
+
+.Request
+----
+  GET /projects/Public-Plugins/children/ HTTP/1.0
+----
+
+As result a list of link:#project-info[ProjectInfo] entries is
+returned that describe the child projects.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#project",
+      "id": "plugins%2Freplication",
+      "name": "plugins/replication",
+      "parent": "Public-Plugins",
+      "description": "Copies to other servers using the Git protocol"
+    },
+    {
+      "kind": "gerritcodereview#project",
+      "id": "plugins%2Freviewnotes",
+      "name": "plugins/reviewnotes",
+      "parent": "Public-Plugins",
+      "description": "Annotates merged commits using notes on refs/notes/review."
+    },
+    {
+      "kind": "gerritcodereview#project",
+      "id": "plugins%2Fsingleusergroup",
+      "name": "plugins/singleusergroup",
+      "parent": "Public-Plugins",
+      "description": "GroupBackend enabling users to be directly added to access rules"
+    }
+  ]
+----
+
+To resolve the child projects of a project recursively the parameter
+`recursive` can be set.
+
+Child projects that are not visible to the calling user are ignored and
+are not resolved further.
+
+.Request
+----
+  GET /projects/Public-Projects/children/?recursive HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#project",
+      "id": "gerrit",
+      "name": "gerrit",
+      "parent": "Public-Projects",
+      "description": "Gerrit Code Review"
+    },
+    {
+      "kind": "gerritcodereview#project",
+      "id": "plugins%2Freplication",
+      "name": "plugins/replication",
+      "parent": "Public-Plugins",
+      "description": "Copies to other servers using the Git protocol"
+    },
+    {
+      "kind": "gerritcodereview#project",
+      "id": "plugins%2Freviewnotes",
+      "name": "plugins/reviewnotes",
+      "parent": "Public-Plugins",
+      "description": "Annotates merged commits using notes on refs/notes/review."
+    },
+    {
+      "kind": "gerritcodereview#project",
+      "id": "plugins%2Fsingleusergroup",
+      "name": "plugins/singleusergroup",
+      "parent": "Public-Plugins",
+      "description": "GroupBackend enabling users to be directly added to access rules"
+    },
+    {
+      "kind": "gerritcodereview#project",
+      "id": "Public-Plugins",
+      "name": "Public-Plugins",
+      "parent": "Public-Projects",
+      "description": "Parent project for plugins/*"
+    }
+  ]
+----
+
+[[get-child-project]]
+Get Child Project
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/children/link:#project-name[\{project-name\}]'
+
+Retrieves a child project. If a non-direct child project should be
+retrieved the parameter `recursive` must be set.
+
+.Request
+----
+  GET /projects/Public-Plugins/children/plugins%2Freplication HTTP/1.0
+----
+
+As response a link:#project-info[ProjectInfo] entity is returned that
+describes the child project.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#project",
+    "id": "plugins%2Freplication",
+    "name": "plugins/replication",
+    "parent": "Public-Plugins",
+    "description": "Copies to other servers using the Git protocol"
+  }
+----
+
 [[dashboard-endpoints]]
 Dashboard Endpoints
 -------------------
@@ -727,6 +1086,12 @@
 IDs
 ---
 
+[[branch-id]]
+\{branch-id\}
+~~~~~~~~~~~~~
+The name of a branch or `HEAD`. The prefix `refs/heads/` can be
+omitted.
+
 [[dashboard-id]]
 \{dashboard-id\}
 ~~~~~~~~~~~~~~~~
@@ -745,42 +1110,144 @@
 JSON Entities
 -------------
 
+[[branch-info]]
+BranchInfo
+~~~~~~~~~~
+The `BranchInfo` entity contains information about a branch.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=========================
+|Field Name  ||Description
+|`ref`       ||The ref of the branch.
+|`revision`  ||The revision to which the branch points.
+|`can_delete`|`false` if not set|
+Whether the calling user can delete this branch.
+|=========================
+
+[[branch-input]]
+BranchInput
+~~~~~~~~~~~
+The `BranchInput` entity contains information for the creation of
+a new branch.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=======================
+|Field Name||Description
+|`ref`     |optional|
+The name of the branch. The prefix `refs/heads/` can be
+omitted. +
+If set, must match the branch ID in the URL.
+|`revision`|optional|
+The base revision of the new branch. +
+If not set, `HEAD` will be used as base revision.
+|=======================
+
 [[config-info]]
 ConfigInfo
 ~~~~~~~~~~
 The `ConfigInfo` entity contains information about the effective project
 configuration.
 
-Fields marked with * are only visible to users who have read access to
-`refs/meta/config`.
-
-[options="header",width="50%",cols="1,6"]
-|======================================
-|Field Name                   |Description
-|`use_contributor_agreements*`|
+[options="header",width="50%",cols="1,^2,4"]
+|=========================================
+|Field Name                  ||Description
+|`description`               |optional|
+The description of the project.
+|`use_contributor_agreements`|optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 authors must complete a contributor agreement on the site before
 pushing any commits or changes to this project.
-|`use_content_merge*`|
+|`use_content_merge`         |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 Gerrit will try to perform a 3-way merge of text file content when a
 file has been modified by both the destination branch and the change
 being submitted. This option only takes effect if submit type is not
 FAST_FORWARD_ONLY.
-|`use_signed_off_by*`|
+|`use_signed_off_by`         |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 each change must contain a Signed-off-by line from either the author or
 the uploader in the commit message.
-|`require_change_id*`|
+|`require_change_id`         |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
 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.
-|`commentlinks`|
-Comment link configuration for the project. Has the same format as the
-link:config-gerrit.html#_a_id_commentlink_a_section_commentlink[commentlink section]
-of `gerrit.config`.
-|======================================
+|`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[
+MaxObjectSizeLimitInfo] entity.
+|`submit_type`               ||
+The default submit type of the project, can be `MERGE_IF_NECESSARY`,
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`CHERRY_PICK`.
+|`state`                     |optional|
+The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. +
+Not set if the project state is `ACTIVE`.
+|`commentlinks`              ||
+Map with the comment link configurations of the project. The name of
+the comment link configuration is mapped to the comment link
+configuration, which has the same format as the
+link:config-gerrit.html#_a_id_commentlink_a_section_commentlink[
+commentlink section] of `gerrit.config`.
+|`theme`                     |optional|
+The theme that is configured for the project as a link:#theme-info[
+ThemeInfo] entity.
+|`actions`                   |optional|
+Actions the caller might be able to perform on this project. The
+information is a map of view names to
+link:rest-api-changes.html#action-info[ActionInfo] entities.
+|=========================================
+
+[[config-input]]
+ConfigInput
+~~~~~~~~~~~
+The `ConfigInput` entity describes a new project configuration.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=========================================
+|Field Name                  ||Description
+|`description`               |optional|
+The new description of the project. +
+If not set, the description is removed.
+|`use_contributor_agreements`|optional|
+Whether authors must complete a contributor agreement on the site
+before pushing any commits or changes to this project. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`use_content_merge`         |optional|
+Whether Gerrit will try to perform a 3-way merge of text file content
+when a file has been modified by both the destination branch and the
+change being submitted. This option only takes effect if submit type is
+not FAST_FORWARD_ONLY. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`use_signed_off_by`         |optional|
+Whether each change must contain a Signed-off-by line from either the
+author or the uploader in the commit message. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`require_change_id`         |optional|
+Whether a 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. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`max_object_size_limit`     |optional|
+The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
+limit] of this project as a link:#max-object-size-limit-info[
+MaxObjectSizeLimitInfo] entity. +
+If set to `0`, the max object size limit is removed. +
+If not set, this setting is not updated.
+|`submit_type`               |optional|
+The default submit type of the project, can be `MERGE_IF_NECESSARY`,
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`CHERRY_PICK`. +
+If not set, the submit type is not updated.
+|`state`                     |optional|
+The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. +
+Not set if the project state is `ACTIVE`. +
+If not set, the project state is not updated.
+|=========================================
 
 [[dashboard-info]]
 DashboardInfo
@@ -881,6 +1348,29 @@
 Not set if there is no parent.
 |================================
 
+[[max-object-size-limit-info]]
+MaxObjectSizeLimitInfo
+~~~~~~~~~~~~~~~~~~~~~~
+The `MaxObjectSizeLimitInfo` entity contains information about the
+link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
+limit] of a project.
+
+[options="header",width="50%",cols="1,^2,4"]
+|===============================
+|Field Name        ||Description
+|`value`           |optional|
+The effective value of the max object size limit as a formatted string. +
+Not set if there is no limit for the object size.
+|`configured_value`|optional|
+The max object size limit that is configured on the project as a
+formatted string. +
+Not set if there is no limit for the object size configured on project
+level.
+|`inherited_value` |optional|
+The max object size limit that is inherited as a formatted string. +
+Not set if there is no global limit for the object size.
+|===============================
+
 [[project-description-input]]
 ProjectDescriptionInput
 ~~~~~~~~~~~~~~~~~~~~~~~
@@ -967,6 +1457,9 @@
 |`require_change_id`         |`INHERIT` if not set|
 Whether the usage of Change-Ids is required for the project (`TRUE`,
 `FALSE`, `INHERIT`).
+|`max_object_size_limit`     |optional|
+Max allowed Git object size for this project.
+Common unit suffixes of 'k', 'm', or 'g' are supported.
 |=========================================
 
 [[project-parent-input]]
@@ -1002,6 +1495,22 @@
 |`size_of_packed_objects`  |Size of packed objects in bytes.
 |======================================
 
+[[theme-info]]
+ThemeInfo
+~~~~~~~~~
+The `ThemeInfo` entity describes a theme.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`css`           |optional|
+The path to the `GerritSite.css` file.
+|`header`        |optional|
+The path to the `GerritSiteHeader.html` file.
+|`footer`        |optional|
+The path to the `GerritSiteFooter.html` file.
+|=============================
+
 
 GERRIT
 ------
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 303fc4b..7eed6ef 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -5,14 +5,22 @@
 The API is suitable for automated tools to build upon, as well as
 supporting some ad-hoc scripting use cases.
 
+See also: link:dev-rest-api.html[REST API Developers' Notes].
+
 Endpoints
 ---------
+link:rest-api-access.html[/access/]::
+  Access Right related REST endpoints
 link:rest-api-accounts.html[/accounts/]::
   Account related REST endpoints
 link:rest-api-changes.html[/changes/]::
   Change related REST endpoints
+link:rest-api-config.html[/config/]::
+  Config related REST endpoints
 link:rest-api-groups.html[/groups/]::
   Group related REST endpoints
+link:rest-api-plugins.html[/plugins/]::
+  Plugin related REST endpoints
 link:rest-api-projects.html[/projects/]::
   Project related REST endpoints
 
@@ -53,9 +61,11 @@
 ----
 
 JSON responses are encoded using UTF-8 and use content type
-`application/json`. The JSON response body starts with a magic prefix
-line that must be stripped before feeding the rest of the response
-body to a JSON parser:
+`application/json`.
+
+To prevent against Cross Site Script Inclusion (XSSI) attacks, the JSON
+response body starts with a magic prefix line that must be stripped before
+feeding the rest of the response body to a JSON parser:
 
 ----
   )]}'
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index a4224bd..c13faa6 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -53,7 +53,7 @@
 
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
 
-  $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+  $ curl -Lo .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 
 Then ensure that the execute bit is set on the hook script:
 
diff --git a/Documentation/user-dashboards.txt b/Documentation/user-dashboards.txt
index 3a12b76..0945153 100644
--- a/Documentation/user-dashboards.txt
+++ b/Documentation/user-dashboards.txt
@@ -44,7 +44,7 @@
 `&` or `;` or `,`
 
 The special `foreach=...` parameter is designed to facilitate
-more easily writting similar queries in a dashboard.  The value of the
+more easily writing similar queries in a dashboard.  The value of the
 foreach parameter will be used in every query in the dashboard by
 appending it to their ends with a space (ANDing it with the queries).
 
@@ -66,7 +66,7 @@
 will be used as name (equivalent to a title in a custom dashboard) for
 the dashboard.
 
-Example dashboard config file `MyProject Dashboard`:
+Example of a dashboard config file:
 
 ----
 [dashboard]
@@ -79,6 +79,8 @@
 
 Once defined, project dashboards are accessible using stable URLs by
 using the project name, refname and pathname of the dashboard via URLs
+, e.g. create a dashboard config file named `Main` and push it
+to `refs/meta/dashboards/Site` branch of All-Projects, then access it
 like:
 ----
   /#/projects/All-Projects,dashboards/Site:Main
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 711d1ac..558dcb5 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -22,6 +22,14 @@
 change notifications to specific subsets, for example `branch:master`
 to only see changes proposed for the master branch.
 
+Notification mails for new changes and new patch sets are not sent to
+the change owner.
+
+Notification mails for comments added on changes are not sent to the user
+who added the comment unless the user has enabled the 'CC Me On Comments I
+Write' option in the user preferences.
+
+
 Project Level Settings
 ----------------------
 
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 781118f..7c4ca52 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -168,6 +168,11 @@
 +
 Changes that match 'MESSAGE' arbitrary string in the commit message body.
 
+[[comment]]
+comment:'TEXT'::
++
+Changes that match 'TEXT' string in any comment left by a reviewer.
+
 [[file]]
 file:^'REGEX'::
 +
@@ -187,12 +192,6 @@
 ones using a bracket expression). For example, to match all XML
 files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
 `file:"^name[1-3].xml"`.
-+
-Currently this operator is only available on a watched project
-and may not be used in the search bar. The same holds true for web UI
-"My > Watched Changes", i. e. file:regex is used over the is:watched
-expression. It never produces any results, because the error message:
-"operator not permitted here: file:regex" is suppressed.
 
 [[has]]
 has:draft::
@@ -278,7 +277,7 @@
 ----------------
 
 Operator values that are not bare words (roughly A-Z, a-z, 0-9, @,
-hypen, dot and underscore) must be quoted for the query parser.
+hyphen, dot and underscore) must be quoted for the query parser.
 
 Quoting is accepted as either double quotes
 (e.g.  `message:"the value"`) or as matched
@@ -291,8 +290,8 @@
 Unless otherwise specified, operators are joined using the `AND`
 boolean operator, thereby restricting the search results.
 
-Parentheses can be used to force a particular precendence on complex
-operator expressions, otherwise OR has higher precendence than AND.
+Parentheses can be used to force a particular precedence on complex
+operator expressions, otherwise OR has higher precedence than AND.
 
 Negation
 ~~~~~~~~
@@ -311,7 +310,7 @@
 OR
 ~~
 The boolean operator `OR` (in all caps) can be used to find changes
-that match either operator.  This increases the nubmer of results
+that match either operator.  This increases the number of results
 that are returned, as more changes are considered.
 
 
@@ -320,15 +319,18 @@
 ------
 Label operators can be used to match approval scores given during
 a code review.  The specific set of supported labels depends on
-the server configuration, however `Code-Review` and `Verified`
-are the default labels provided out of the box.
+the server configuration, however the `Code-Review` label is provided
+out of the box.
 
 A label name is any of the following:
 
 * The label name.  Example: `label:Code-Review`.
 
-* The one or two character abbreviation shown in the column header
-  of change list pages.  Example: `label:R` or `label:V`.
+* The label name followed by a ',' followed by a reviewer id or a
+  group id.  To make it clear whether a user or group is being looked
+  for, precede the value by a user or group argument identifier
+  ('user=' or 'group=').  If an LDAP group is being referenced make
+  sure to use 'ldap/<groupname>'.
 
 A label name must be followed by a score, or an operator and a score.
 The easiest way to explain this is by example.
@@ -356,7 +358,20 @@
 +
 Matches changes with either a +1, +2, or any higher score.
 
-`label:Code-Review<=-1`::
+`label:CodeReview=+2,aname`::
++
+Matches changes with a +2 code review where the reviewer or group is aname.
+
+`label:CodeReview=2,user=jsmith`::
++
+Matches changes with a +2 code review where the reviewer is jsmith.
+
+`label:CodeReview=+1,group=ldap/linux.workflow`::
++
+Matches changes with a +1 code review where the reviewer is in the
+ldap/linux.workflow group.
+
+`label:CodeReview<=-1`::
 +
 Matches changes with either a -1, -2, or any lower score.
 
diff --git a/Documentation/user-signedoffby.txt b/Documentation/user-signedoffby.txt
index d07516a..56858bf 100644
--- a/Documentation/user-signedoffby.txt
+++ b/Documentation/user-signedoffby.txt
@@ -104,10 +104,10 @@
 mergers will sometimes manually convert an acker's "yep, looks good to me"
 into an Acked-by:.
 
-Acked-by: does not necessarily indicate acknowledgement of the entire patch.
+Acked-by: does not necessarily indicate acknowledgment of the entire patch.
 For example, if a patch affects multiple subsystems and has an Acked-by: from
-one subsystem maintainer then this usually indicates acknowledgement of just
-the part which affects that maintainer's code.  Judgement should be used here.
+one subsystem maintainer then this usually indicates acknowledgment of just
+the part which affects that maintainer's code.  Judgment should be used here.
 When in doubt people should refer to the original discussion in the mailing
 list archives.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
new file mode 100644
index 0000000..d183cca
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -0,0 +1,756 @@
+Release notes for Gerrit 2.8
+============================
+
+
+Gerrit 2.8 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8-rc0.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.8-rc0.war]
+
+
+Schema Change
+-------------
+
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.8.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.8.x.  If you are upgrading from 2.2.x.x or
+later, you may ignore this warning and upgrade directly to 2.8.x.
+
+*WARNING:* The replication plugin now automatically creates missing repositories
+on the destination if during the replication of a ref the target repository is
+found to be missing. This is a change in behavior of the replication plugin. To go
+back to the old behavior, set the parameter `remote.NAME.createMissingRepositories`
+in the `replication.config` file to `false`.
+
+
+Release Highlights
+------------------
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/intro-change-screen.html[
+New change screen] with completely redesigned UI and fully using the REST API.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#index[
+Secondary indexing with Lucene and Solr].
+
+* Lots of new link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api.html[
+REST API endpoints].
+
+* New
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#ui_extension[
+UI extension] and
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/js-api.html[
+JavaScript API] for plugins.
+
+* New build system using Facebook's link:http://facebook.github.io/buck/[Buck].
+
+* New core plugin: Download Commands.
+
+
+New Features
+------------
+
+Build
+~~~~~
+
+* Gerrit is now built with
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-buck.html[
+Buck].
+
+* Documentation is now built with Buck and link:http://asciidoctor.org[Asciidoctor].
+
+
+Indexing and Search
+~~~~~~~~~~~~~~~~~~~
+
+Gerrit can be configured to use a
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#index[
+secondary index] with Lucene or Solr.
+
+Existing search operations use the secondary index, when enabled, to increase
+performance and reduce resource usage.
+
+The following additional search operations are possible when secondary indexing
+is enabled:
+
+* New
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/user-search.html#comment[
+`comment` search operator].
+
+* The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/user-search.html#file[
+`file` operator] can be used to find changes on the specified file.
+
+* Regular expressions are allowed in `file` searches.
+
+
+Configuration
+~~~~~~~~~~~~~
+
+* Project owners can define `receive.maxObjectSizeLimit` in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#receive.maxObjectSizeLimit[
+project configuration] to further reduce the global setting.
+
+* Site administrators can define a
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-mail.html#_footer_vm[
+footer template] that will be appended to the end of all outgoing emails after
+the 'ChangeFooter' and 'CommentFooter'.
+
+* New `topic-changed` hook and stream event is fired when a change's topic is
+edited from the Web UI or via a REST API.
+
+* New options `--list-plugins` and `--install-plugins` on the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/pgm-init.html[
+site initialization command].
+
+* New `auth.httpDisplaynameHeader` and `auth.httpEmailHeader` in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#__a_id_auth_a_section_auth[
+authentication configuration].
++
+When using HTTP-based authentication, the SSO can be delegated to check not only
+the user credentials but also to fetch the full user-profile.
++
+With the config properties `auth.httpDisplaynameHeader` and `auth.httpEmailHeader`
+it is possible to configure the name of the headers used for propagating this extra
+information and enforce them on the user profile during login and beyond.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#__a_id_httpd_a_section_httpd[
+Customizable registration page for HTTP authentication].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#__a_id_httpd_a_section_httpd[
+Configurable external `robots.txt` file].
+
+* Support for
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/database-setup.html#createdb_oracle[
+Oracle database].
+
+* New bash completion script for autocompletion of parameters to the gerrit.sh wrapper.
+
+* The site can be
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-auto-site-initialization.html[
+auto-initialized on server startup].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#httpd.filterClass[
+Configurable filtering of HTTP traffic through Gerrit's HTTP protocol].
+
+* Labels can be
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-labels.html#httpd.label_copyAllScoresIfNoCodeChange[
+configured to copy scores forward to new patch sets if there is no code change].
+
+* Labels can be
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-labels.html#httpd.label_copyAllScoresOnTrivialRebase[
+configured to copy scores forward to new patch sets for trivial rebases].
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+* The change status is shown in a separate column on dashboards and search results.
+
+Change Screens
+^^^^^^^^^^^^^^
+
+
+* New change screen with completely redesigned UI, using the REST API.
++
+Site administrators can
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#gerrit.changeScreen[
+configure which change screen is shown by default].
++
+Users can choose which one to use in their personal preferences, either using
+the site default or explicitly choosing the old one or new one.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=141[Issue 141]:
+In the new change screen, comments can be added on a range of lines.
+
+* New button to cherry-pick the change to another branch.
+
+* When issuing a rebase via the Web UI, the committer is now the logged in
+  user, rather than "Gerrit Code Review".
++
+If the user has more than one email address, the preferred email address will
+be used.
+
+* Default user's full name to git committer name if user has not configured a
+full name in their profile.
+
+* Include comment author attributes in comment panels.
++
+Comment author's email address and name are included as attributes in comment
+panels.  This makes it easier to filter out CI-based comments using user
+scripts.
+
+* Copy reviewed flag to new patch sets for identical files.
++
+If a user has already seen and reviewed a file, the 'reviewed' flag is forwarded
+on to the next patch set when the content of the file in the next patch set is
+identical to the reviewed file.
+
+* "Uploaded Patch Set 1" change message is added on changes when they
+are uploaded.
+
+
+REST API
+~~~~~~~~
+
+* Several new link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api.html[
+REST API endpoints] are added.
+
+* REST views can determine how long their response should be cached.
+
+* REST views can handle 'HTTP 422 Unprocessable Entity' responses.
+
+Access Rights
+^^^^^^^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-access.html#list-access[
+List access rights for project(s)]
+
+Accounts
+^^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#create-account[
+Create account]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#get-account-name[
+Get account full name]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#set-account-name[
+Set account full name]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#delete-account-name[
+Delete account full name]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#list-account-emails[
+List account email addresses]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#get-account-email[
+Get account email address]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#set-preferred-email[
+Set account preferred email address]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#create-account-email[
+Create account email]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#delete-account-email[
+Delete account email]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#get-active[
+Get account state]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#set-active[
+Set account state to active]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#delete-active[
+Set account state to inactive]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#get-http-password[
+Get account HTTP password]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#set-http-password[
+Set or generate account HTTP password]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#delete-http-password[
+Delete account HTTP password]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#list-ssh-keys[
+List account SSH keys]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#get-ssh-key[
+Get account SSH key]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#add-ssh-key[
+Add account SSH key]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#delete-ssh-key[
+Delete account SSH key]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#get-username[
+Get account username]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#get-starred-changes[
+Get starred changes]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#star-change[
+Star change]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-accounts.html#unstar-change[
+Unstar change]
+
+Changes
+^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#rebase-change[
+Rebase change]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#cherry-pick[
+Cherry-pick revision]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#get-content[
+Get content of a file in a revision]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#get-patch[
+Get revision as a formatted patch]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#get-diff[
+Get diff of a file in a revision]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#get-commit[
+Get parsed commit of a revision]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#publish-draft-change[
+Publish draft change]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#delete-draft-change[
+Delete draft change]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#suggest-reviewers[
+Suggest reviewers]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-changes.html#get-included-in[
+Get included in]
+
+
+Config
+^^^^^^
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-config.html#get-capabilities[
+Get capabilities]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-config.html#get-version[
+Get version] (of the Gerrit server)
+
+
+Projects
+^^^^^^^^
+
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#list-branches[
+List branches]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#get-branch[
+Get branch]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#create-branch[
+Create branch]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#delete-branch[
+Delete branch]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#list-child-projects[
+List child projects]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#get-child-project[
+Get child project]
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/rest-api-projects.html#set-config[
+Set configuration]
+
+
+Capabilities
+~~~~~~~~~~~~
+
+
+New global capabilities are added.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/access_control.html#capability_generateHttpPassword[
+Generate Http Password] allows non-administrator users to generate HTTP
+passwords for users other than themselves.
++
+This capability would typically be assigned to a non-interactive group
+to be able to generate HTTP passwords for users from a tool or web service
+that uses the Gerrit REST API.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/access_control.html#capability_runAs[
+Run As] allows users to impersonate other users by setting the `X-Gerrit-RunAs`
+HTTP header on REST API calls.
++
+Site administrators do not inherit this capability;  it must be granted
+explicitly.
+
+
+Emails
+~~~~~~
+
+* The `RebasedPatchSet` template is removed.  Email notifications for rebased
+changes are now sent with the `ReplacePatchSet` template.
+
+* Comment notification emails now include context of comments that are replied
+to, and links to the file(s) in which comments are made.
+
+
+Plugins
+~~~~~~~
+
+
+Global
+^^^^^^
+
+
+* Plugins may now contribute buttons to various parts of the UI using the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#ui_extension[
+UI extension] and
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/js-api.html[
+JavaScript API].
+
+* Plugins may now provide an 'About' section on their documentation index page.
+
+* Plugins may now provide separate sections for REST API and servlet
+documentation on their index page.
+
+* Plugins may now provide
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-validation.html#pre-merge-validation[
+pre-merge validation steps].
+
+* Plugins may now provide
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#capabilities[
+Global capabilities].
+
+* Plugins may now
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#plugin_name[
+define their own name] and get the name injected at runtime.
+
+* The "hello world" plugin is replaced with the "cookbook plugin" which has more
+examples of the plugin API's usage.
+
+* Plugins may now trigger and listen to a "project deleted"
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#events[
+event].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2101[Issue 2101]:
+Plugins implementing LifecycleListener can use auto registration.
+
+* Plugins may bind REST endpoints with empty view names.
+
+* Plugins may now provide
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#top-menu-extensions[
+entries in Gerrit's top menu].
+
+* Plugins may now
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#stream-events[
+send events to the events stream].
+
+* Plugins may now bind multiple SSH commands to the same implementation class.
+
+* Plugins may now provide
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/dev-plugins.html#download-commands[
+download schemes and download commands].
++
+Commonly used download schemes and commands are moved out of core
+Gerrit and are now implemented by a new core plugin, `download-commands`.
+
+
+
+Commit Message Length Checker
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+
+* Commits whose subject or body length exceeds the limit can be rejected.
+
+Replication
+^^^^^^^^^^^
+
+* Automatically create missing repositories on the destination.
++
+If during the replication of a ref the target repository is found to be missing,
+the repository is automatically created.
++
+This is a change in behavior of the replication plugin. To go back to the old
+behavior, set the parameter `remote.NAME.createMissingRepositories` in the
+`replication.config` file to `false`.
+
+* Support for replication of project deletions.
++
+The replication plugin can now be configured to listen to project deletion events
+and to replicate the project deletions. By default project deletions are *not*
+replicated.
+
+* The `{$name}` placeholder is optional when replicating a single project,
+allowing a single project to be replicated under a different name.
+
+* Project names can be matched with wildcard or regex patterns in `replication.config`.
+
+* The `replication start` command does not exit until replication is finished
+when the `--wait` option is used.
+
+* The `replication start` command displays a summary of the replication status.
+
+* Retry counts are added to replication task names, so they can be seen in the
+output of the `show-queue` command.
+
+* The `remoteNameStyle` option can be set to `basenameOnly` to replicate projects
+using only the basename on the target server.
+
+* The `startReplication` global capability is now provided by the plugin.
+
+* Pushes to each destination URI are serialized.
++
+Scheduling a retry to avoid collision with an in-flight push is differentiated
+from a retry due to a transport error.  In the case of collision avoidance, the
+job is rescheduled according to the replication delay, rather than the retry
+delay.
+
+
+ssh
+~~~
+
+
+* The `commit-msg` hook installation command is now
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-gerrit.html#gerrit.installCommitMsgHookCommand[
+configurable].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/cmd-ls-members.html[
+New `ls-members` command].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/cmd-set-members.html[
+New `set-members` command].
++
+New command to manipulate group membership. Members can be added or removed
+and groups can be included or excluded in one specific group or number of groups.
+
+* The full commit message is now included in the data sent by the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/cmd-stream-events.html[
+`stream-events` command].
+
+* The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/cmd-show-queue.html[
+`show-queue` command] now shows the time that a task was added to the queue.
+
+
+Daemon
+~~~~~~
+
+
+* Add `--init` option to Daemon to initialize site on daemon start.
++
+The `--init` option will also upgrade an already existing site and is processed in
+non-interactive (batch) mode.
+
+
+Bug Fixes
+---------
+
+
+General
+~~~~~~~
+
+
+* Use the parent change on the same branch for rebases.
++
+Since there can be multiple changes with the same commit on different branches,
+use the parent change on the same branch during rebase.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]:
+Fix change stuck in SUBMITTED state but actually merged.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1699[Issue 1699]:
+Fix handling of projects with trailing ".git" suffix.
+
+* Limit retrying of submitted changes to 12 hours.
+
+* Don't allow project owners to delete branches if force push is blocked.
+
+
+Configuration
+~~~~~~~~~~~~~
+
+
+* Do not persist default project state in `project.config`.
+
+* Honor the `gerrit.cannonicalWebUrl` setting when opening the browser after init.
+
+* Fix 'query disabled' error when Query Limit is set.
+
+* Honor the `gerrit.createChangeId` setting from the git config in the
+The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/cmd-hook-commit-msg.html[
+`commit-msg` hook].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2045[Issue 2045]:
+Define user scope when parsing server config.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1990[Issue 1990]:
+Support optional Certificate Revocation List (CRL) with `CLIENT_SSL_CERT_LDAP`.
+
+* Do not override error and gc logging configuration provided by the
+`-Dlog4j.configuration` parameter.
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1574[Issue 1574]:
+Correctly highlight matches of text in escaped HTML entities in suggestion results.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1996[Issue 1996]:
+The "Keyboard Shortcuts" help popup can be closed by pressing the Escape key.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2013[Issue 2013]:
+Correctly populate the list of watched changes when watching more than one project.
+
+* Display "Working..." when header is hidden.
+
+Change Screens
+^^^^^^^^^^^^^^
+
+
+* Default review comment visibility is changed to expand all recent.
++
+By default all comments within the last week are expanded, rather than
+only the most recent.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1814[Issue 1814]:
+Sort labels alphabetically by name in the approval table.
+
+* Don't add "This patchset was cherry picked to ..." for the same change.
++
+If a patchset is cherry-picked to the same destination branch and
+ends up on the same change, it does not make sense to add the "This
+patchset was cherry picked to change ..." message.
++
+In this case, it makes more sense for the message to say "Uploaded
+patch set N" instead.
+
+Project Screens
+^^^^^^^^^^^^^^^
+
+
+* Only enable the delete branch button when branches are selected.
+
+* Disable the delete branch button while branch deletion requests are
+still being processed.
+
+User Profile Screens
+^^^^^^^^^^^^^^^^^^^^
+
+
+* The preferred email address field is shown as empty if the user has no
+preferred email address.
+
+
+REST API
+~~~~~~~~
+
+
+* Support raw input also in POST requests.
+
+* Show granted date for labels/all when using `/changes/`.
+
+* Return all revisions when `o=ALL_REVISIONS` is set on `/changes/`.
+
+ssh
+~~~
+
+
+* The `--force-message` option is removed from the
+The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/cmd-review.html[
+`review` command].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1908[Issue 1908]:
+Provide more informative error messages when rejecting updates.
+
+* Remove the limit in the query of patch sets by revision.
+
+* Add `isDraft` in the `patchSet` attribute of `stream-events` data.
++
+This allows consumers of the event stream to determine whether or not
+the event is related to a draft patch set.
+
+* Normalize the case of review labels submitted via the
+The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/cmd-review.html[
+`review` command].
+
+* The `@CommandMetaData(descr)` annotation is deprecated in favor of `@CommandMetaData(description)`.
+
+* Improve the error message when rejecting upload for review to a read-only project.
+
+
+Plugins
+~~~~~~~
+
+Global
+^^^^^^
+
+* Better error message when a Javascript plugin cannot be loaded.
+
+* Plugin documentation links are opened in a new tab.
+
+* The GitReferenceUpdatedListener.Event API is simplified.
++
+The Event exposed the getUpdates method which implied that one Event
+could contain updates of more than one reference. However, this feature
+was never used.
++
+The API is simplified in the sense that one Event now corresponds to
+one ref update only.
+
+
+Review Notes
+^^^^^^^^^^^^
+
+* Do not try to create review notes for ref deletion events.
+
+* Fix committing the notes from the export command.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2087[Issue 2087]:
+Fix note creation when the same commit exists in another Git repository.
+
+* Improve the export command performance.
+
+* Create review note also when newObjectId already present in another branch.
+
+Emails
+~~~~~~
+
+* Email notifications are sent for new changes created via actions in the
+Web UI such as cherry-picking or reverting a change.
+
+
+Tools
+~~~~~
+
+
+* git-exproll.sh: return non-zero on errors
+
+
+Documentation
+-------------
+
+
+* The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/index.html[
+documentation index page] is rewritten in a hierarchical structure.
+
+* Documentation of
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/config-project-config.txt[
+project configuration] is added.
+
+* Various spelling mistakes are corrected in the documentation and previous
+release notes.
+
+
+Upgrades
+--------
+
+* Update JGit to 3.1.0.201310021548-r
+* Update gwtorm to 1.7
+* Update guice to 4.0-beta
+* Update guava to 15.0
+* Update H2 to 1.3.173
+* Update bouncycastle to 1.44
+* Update Apache Mina to 2.0.7
+* Update Apache SSHD to 0.9.0.201311081
+* asciidoctor 0.1.4 is now required to build the documentation
+* jsr305 library was removed
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index b656be2..28b9f65 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_8]]
+Version 2.8.x
+-------------
+* link:ReleaseNotes-2.8.html[2.8]
+
 [[2_7]]
 Version 2.7.x
 -------------
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..23c7033
--- /dev/null
+++ b/VERSION
@@ -0,0 +1,5 @@
+# Maven style API version (e.g. '2.x-SNAPSHOT').
+# Used by :api_install and :api_deploy targets
+# when talking to the destination repository.
+#
+GERRIT_VERSION = '2.8-SNAPSHOT'
diff --git a/contrib/.pep8rc b/contrib/.pep8rc
new file mode 100644
index 0000000..568bcfb
--- /dev/null
+++ b/contrib/.pep8rc
@@ -0,0 +1,5 @@
+[pep8]
+max_line_length = 80
+show_pep8 = True
+show_source = True
+ignore = E111,E121
diff --git a/contrib/bash_completion b/contrib/bash_completion
new file mode 100644
index 0000000..6772235
--- /dev/null
+++ b/contrib/bash_completion
@@ -0,0 +1,73 @@
+# The MIT License
+#
+# Copyright (C) 2013 Sony Mobile Communications. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# #########################################################################
+# This bash script adds tab-completion to the gerrit.sh script.
+#
+# Testing it out without installing
+# =================================
+#
+# To test out the completion without "installing" this, just run this file
+# directly, like so:
+#
+#     . ~/path/to/bash_completion
+#
+# Note: There's a dot ('.') at the beginning of that command.
+#
+# After you do that, tab completion will immediately be made available in your
+# current Bash shell. It will not, however, be available next time you log in.
+#
+# Installing
+# ==========
+#
+# To install the completion, copy this file to the appropriate folder for
+# your distribution, for example:
+#
+#     cp ~/path/to/bash_completion /etc/bash_completion.d/gerrit_sh
+#
+# Alternatively you can invoke this file from your .bash_profile, like so:
+#
+#     . ~/path/to/bash_completion
+#
+# Do the same in your .bashrc if .bashrc doesn't invoke .bash_profile.
+#
+# The settings will take effect the next time you log in.
+#
+# Uninstalling
+# ============
+#
+# To uninstall, just remove the file from the bash completion folder, or
+# remove the line from your .bash_profile and .bashrc.
+# #########################################################################
+
+_gerrit_sh()
+{
+    local cur prev opts
+    COMPREPLY=()
+    cur="${COMP_WORDS[COMP_CWORD]}"
+    prev="${COMP_WORDS[COMP_CWORD-1]}"
+    opts="check restart run start status stop supervise"
+
+    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+}
+complete -F _gerrit_sh gerrit.sh
+
diff --git a/contrib/check-valid-commit.py b/contrib/check-valid-commit.py
index ca1785e..150b310 100755
--- a/contrib/check-valid-commit.py
+++ b/contrib/check-valid-commit.py
@@ -1,5 +1,8 @@
 #!/usr/bin/env python
-import commands
+
+from __future__ import print_function
+
+import subprocess
 import getopt
 import sys
 
@@ -24,8 +27,8 @@
     try:
         opts, args = getopt.getopt(sys.argv[1:], '', \
             ['change=', 'project=', 'branch=', 'commit=', 'patchset='])
-    except getopt.GetoptError, err:
-        print 'Error: %s' % (err)
+    except getopt.GetoptError as err:
+        print('Error: %s' % (err))
         usage()
         sys.exit(-1)
 
@@ -41,7 +44,7 @@
         elif arg == '--patchset':
             patchset = value
         else:
-            print 'Error: option %s not recognized' % (arg)
+            print('Error: option %s not recognized' % (arg))
             usage()
             sys.exit(-1)
 
@@ -51,11 +54,11 @@
         sys.exit(-1)
 
     command = 'git cat-file commit %s' % (commit)
-    status, output = commands.getstatusoutput(command)
+    status, output = subprocess.getstatusoutput(command)
 
     if status != 0:
-        print 'Error running \'%s\'. status: %s, output:\n\n%s' % \
-            (command, status, output)
+        print('Error running \'%s\'. status: %s, output:\n\n%s' % \
+            (command, status, output))
         sys.exit(-1)
 
     commitMessage = output[(output.find('\n\n')+2):]
@@ -74,21 +77,21 @@
     passes(commit)
 
 def usage():
-    print 'Usage:\n'
-    print sys.argv[0] + ' --change <change id> --project <project name> ' \
-        + '--branch <branch> --commit <sha1> --patchset <patchset id>'
+    print('Usage:\n')
+    print(sys.argv[0] + ' --change <change id> --project <project name> ' \
+        + '--branch <branch> --commit <sha1> --patchset <patchset id>')
 
 def fail( commit, message ):
     command = SSH_COMMAND + FAILURE_SCORE + ' -m \\\"' \
         + _shell_escape( FAILURE_MESSAGE + '\n\n' + message) \
         + '\\\" ' + commit
-    commands.getstatusoutput(command)
+    subprocess.getstatusoutput(command)
     sys.exit(1)
 
 def passes( commit ):
     command = SSH_COMMAND + PASS_SCORE + ' -m \\\"' \
         + _shell_escape(PASS_MESSAGE) + ' \\\" ' + commit
-    commands.getstatusoutput(command)
+    subprocess.getstatusoutput(command)
 
 def _shell_escape(x):
     s = ''
diff --git a/contrib/git-exproll.sh b/contrib/git-exproll.sh
index 9526d9f..066c57c 100644
--- a/contrib/git-exproll.sh
+++ b/contrib/git-exproll.sh
@@ -126,7 +126,7 @@
 
     [ -n "$1" ] && info "ERROR $1"
 
-    exit
+    exit 128
 }
 
 debug() { [ -n "$SW_V" ] && info "$1" ; }
diff --git a/contrib/themes/diffy/etc/GerritSite.css b/contrib/themes/diffy/etc/GerritSite.css
index d476957..76c595a 100644
--- a/contrib/themes/diffy/etc/GerritSite.css
+++ b/contrib/themes/diffy/etc/GerritSite.css
@@ -15,15 +15,15 @@
 #gerrit_topmenu {
   left: 60px;
   margin-right: 60px;
-  padding-right: 10px;
   position: relative;
+  margin-bottom: 5px;
 }
 
 #diffy_logo {
   display: block !important;
   margin-bottom: -55px;
-  padding-left: 20px;
+  padding-left: 10px;
   position: relative;
-  top: -45px;
+  top: -55px;
   width: 60px;
 }
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index 30e60af..c97172e 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -36,6 +36,8 @@
 
 """
 
+from __future__ import print_function
+
 import argparse
 import json
 import re
@@ -95,7 +97,7 @@
     try:
       process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
       std_out, std_err = process.communicate()
-    except OSError, e:
+    except OSError as e:
       raise self.CheckCallError(command, cwd, e.errno, None)
     if process.returncode:
       raise self.CheckCallError(command, cwd, process.returncode, std_out, std_err)
@@ -107,9 +109,9 @@
                 '--format', 'JSON', '-c', sql_query]
     try:
       (gsql_out, _gsql_stderr) = self.CheckCall(gsql_cmd)
-    except self.CheckCallError, e:
-      print "return code is %s" % e.retcode
-      print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr)
+    except self.CheckCallError as e:
+      print("return code is %s" % e.retcode)
+      print("stdout and stderr is\n%s%s" % (e.stdout, e.stderr))
       raise
 
     new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
@@ -194,7 +196,7 @@
     prev_patch_id = self.GetPatchId(prev_revision)
     cur_patch_id = self.GetPatchId(self.commit)
     if prev_patch_id == '0' and cur_patch_id == '0':
-      print "commits %s and %s are both empty or merge commits" % (prev_revision, self.commit)
+      print("commits %s and %s are both empty or merge commits" % (prev_revision, self.commit))
       return
     if cur_patch_id != prev_patch_id:
       # patch-ids don't match
@@ -238,7 +240,7 @@
 
     gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
                           "detection script.\'")
-    for acct, flags in self.acct_approvals.items():
+    for acct, flags in list(self.acct_approvals.items()):
       gerrit_review_cmd = ['gerrit', 'review', '--project', self.project,
                             '--message', gerrit_review_msg, flags, self.commit]
       email_addr = self.GetEmailFromAcctId(acct)
@@ -247,5 +249,5 @@
 if __name__ == "__main__":
   try:
     TrivialRebase().Run()
-  except AssertionError, e:
-    print >> sys.stderr, e
+  except AssertionError as e:
+    print(e, file=sys.stderr)
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
new file mode 100644
index 0000000..cb946d6
--- /dev/null
+++ b/gerrit-acceptance-tests/BUCK
@@ -0,0 +1,42 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+java_library(
+  name = 'lib',
+  srcs = glob(['src/test/java/com/google/gerrit/acceptance/*.java']),
+  deps = [
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-launcher:launcher',
+    '//gerrit-lucene:lucene',
+    '//gerrit-httpd:httpd',
+    '//gerrit-pgm:init-base',
+    '//gerrit-pgm:pgm',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-server:testutil',
+    '//gerrit-sshd:sshd',
+
+    '//lib:args4j',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:h2',
+    '//lib:jsch',
+    '//lib:junit',
+    '//lib:servlet-api-3_0',
+
+    '//lib/commons:httpclient',
+    '//lib/commons:httpcore',
+    '//lib/log:impl_log4j',
+    '//lib/log:log4j',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/jgit:junit',
+    '//lib/mina:sshd',
+  ],
+  export_deps = True,
+  visibility = [
+    '//tools/eclipse:classpath',
+    '//gerrit-acceptance-tests/...',
+  ],
+)
diff --git a/gerrit-acceptance-tests/pom.xml b/gerrit-acceptance-tests/pom.xml
deleted file mode 100644
index 63facba..0000000
--- a/gerrit-acceptance-tests/pom.xml
+++ /dev/null
@@ -1,148 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-acceptance-tests</artifactId>
-
-  <name>Gerrit Code Review - Acceptance Tests</name>
-
-  <description>
-    Gerrit Acceptance Tests
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <scope>test</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-reviewdb</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-main</artifactId>
-      <version>${project.version}</version>
-      <scope>runtime</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-server</artifactId>
-      <version>${project.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>org.apache.tomcat</groupId>
-          <artifactId>servlet-api</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-log4j12</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-openid</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-sshd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-httpd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-pgm</artifactId>
-      <version>${project.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>org.eclipse.jetty</groupId>
-          <artifactId>jetty-servlet</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jetty</groupId>
-      <artifactId>jetty-servlet</artifactId>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-
-  <profiles>
-    <profile>
-      <id>acceptance</id>
-      <activation>
-        <property>
-          <name>!gerrit.acceptance-tests.skip</name>
-        </property>
-      </activation>
-      <build>
-        <plugins>
-          <plugin>
-            <groupId>org.apache.maven.plugins</groupId>
-            <artifactId>maven-failsafe-plugin</artifactId>
-            <version>2.5</version>
-            <executions>
-              <execution>
-                <id>integration-test</id>
-                <goals>
-                  <goal>integration-test</goal>
-                </goals>
-              </execution>
-              <execution>
-                <id>verify</id>
-                <goals>
-                  <goal>verify</goal>
-                </goals>
-              </execution>
-            </executions>
-          </plugin>
-        </plugins>
-      </build>
-    </profile>
-  </profiles>
-</project>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 51c7b3d..784b461 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,22 +14,52 @@
 
 package com.google.gerrit.acceptance;
 
-import org.junit.After;
-import org.junit.Before;
-
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 
 public abstract class AbstractDaemonTest {
+  protected GerritServer server;
 
-  private GerritServer server;
+  @Rule
+  public TestRule testRunner = new TestRule() {
+    @Override
+    public Statement apply(final Statement base, final Description description) {
+      return new Statement() {
+        @Override
+        public void evaluate() throws Throwable {
+          boolean mem = description.getAnnotation(UseLocalDisk.class) == null;
+          beforeTest(config(description), mem);
+          base.evaluate();
+          afterTest();
+        }
+      };
+    }
+  };
 
-  @Before
-  public final void beforeTest() throws Exception {
-    server = GerritServer.start();
+  private static Config config(Description description) {
+    GerritConfigs cfgs = description.getAnnotation(GerritConfigs.class);
+    GerritConfig cfg = description.getAnnotation(GerritConfig.class);
+    if (cfgs != null && cfg != null) {
+      throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig not both");
+    }
+    if (cfgs != null) {
+      return ConfigAnnotationParser.parse(cfgs);
+    } else if (cfg != null) {
+      return ConfigAnnotationParser.parse(cfg);
+    } else {
+      return null;
+    }
+  }
+
+  private void beforeTest(Config cfg, boolean memory) throws Exception {
+    server = GerritServer.start(cfg, memory);
     server.getTestInjector().injectMembers(this);
   }
 
-  @After
-  public final void afterTest() throws Exception {
+  private void afterTest() throws Exception {
     server.stop();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index f56ef07..12add3e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -30,8 +30,10 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
+
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.KeyPair;
@@ -77,7 +79,7 @@
         db.accountExternalIds().insert(Collections.singleton(extMailto));
       }
 
-      Account a = new Account(id);
+      Account a = new Account(id, TimeUtil.nowTs());
       a.setFullName(fullName);
       a.setPreferredEmail(email);
       db.accounts().insert(Collections.singleton(a));
@@ -114,6 +116,12 @@
     return create(username, null, username, (String[]) null);
   }
 
+  public TestAccount admin()
+      throws UnsupportedEncodingException, OrmException, JSchException {
+    return create("admin", "admin@example.com", "Administrator",
+      "Administrators");
+  }
+
   private AccountExternalId.Key getEmailKey(String email) {
     return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
new file mode 100644
index 0000000..cf60fb4
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.ArrayList;
+
+class ConfigAnnotationParser {
+
+  private static Splitter splitter = Splitter.on(".").trimResults();
+
+  static Config parse(GerritConfigs annotation) {
+    if (annotation == null) {
+      return null;
+    }
+
+    Config cfg = new Config();
+    for (GerritConfig c : annotation.value()) {
+      parse(cfg, c);
+    }
+    return cfg;
+  }
+
+  static Config parse(GerritConfig annotation) {
+    Config cfg = new Config();
+    parse(cfg, annotation);
+    return cfg;
+  }
+
+  static private void parse(Config cfg, GerritConfig c) {
+    ArrayList<String> l = Lists.newArrayList(splitter.split(c.name()));
+    if (l.size() == 2) {
+      cfg.setString(l.get(0), null, l.get(1), c.value());
+    } else if (l.size() == 3) {
+      cfg.setString(l.get(0), l.get(1), l.get(2), c.value());
+    } else {
+      throw new IllegalArgumentException(
+          "GerritConfig.name must be of the format"
+              + " section.subsection.name or section.name");
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
new file mode 100644
index 0000000..5cb1229
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+public @interface GerritConfig {
+  String name();
+  String value();
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
new file mode 100644
index 0000000..58bb9f2
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+public @interface GerritConfigs {
+  public GerritConfig[] value();
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 65bab6a..daf9d38 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -14,10 +14,29 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.pgm.Daemon;
+import com.google.gerrit.pgm.Init;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.util.SocketUtil;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Field;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.net.UnknownHostException;
 import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CyclicBarrier;
@@ -25,22 +44,11 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.pgm.Daemon;
-import com.google.gerrit.pgm.Init;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.inject.Injector;
-import com.google.inject.Module;
-
-class GerritServer {
+public class GerritServer {
 
   /** Returns fully started Gerrit server */
-  static GerritServer start() throws Exception {
-
-    final String sitePath = initSite();
-
+  static GerritServer start(Config base, boolean memory) throws Exception {
     final CyclicBarrier serverStarted = new CyclicBarrier(2);
-
     final Daemon daemon = new Daemon(new Runnable() {
       public void run() {
         try {
@@ -53,35 +61,76 @@
       }
     });
 
-    ExecutorService daemonService = Executors.newSingleThreadExecutor();
-    daemonService.submit(new Callable<Void>() {
-      public Void call() throws Exception {
-        int rc = daemon.main(new String[] {"-d", sitePath, "--headless" });
-        if (rc != 0) {
-          System.out.println("Failed to start Gerrit daemon. Check "
-              + sitePath + "/logs/error_log");
-          serverStarted.reset();
-        }
-        return null;
-      };
-    });
-
-    serverStarted.await();
-    System.out.println("Gerrit Server Started");
+    final File site;
+    ExecutorService daemonService = null;
+    if (memory) {
+      site = null;
+      Config cfg = base != null ? base : new Config();
+      mergeTestConfig(cfg);
+      cfg.setBoolean("httpd", null, "requestLog", false);
+      cfg.setBoolean("sshd", null, "requestLog", false);
+      daemon.setDatabaseForTesting(ImmutableList.<Module>of(
+          new InMemoryTestingDatabaseModule(cfg)));
+      daemon.start();
+    } else {
+      site = initSite(base);
+      daemonService = Executors.newSingleThreadExecutor();
+      daemonService.submit(new Callable<Void>() {
+        public Void call() throws Exception {
+          int rc = daemon.main(new String[] {"-d", site.getPath(), "--headless" });
+          if (rc != 0) {
+            System.out.println("Failed to start Gerrit daemon. Check "
+                + site.getPath() + "/logs/error_log");
+            serverStarted.reset();
+          }
+          return null;
+        };
+      });
+      serverStarted.await();
+      System.out.println("Gerrit Server Started");
+    }
 
     Injector i = createTestInjector(daemon);
-    return new GerritServer(i, daemon, daemonService);
+    return new GerritServer(site, i, daemon, daemonService);
   }
 
-  private static String initSite() throws Exception {
-    DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
-    String path = "target/test_site_" + df.format(new Date());
+  private static File initSite(Config base) throws Exception {
+    File tmp = TempFileUtil.createTempDirectory();
     Init init = new Init();
-    int rc = init.main(new String[] {"-d", path, "--batch", "--no-auto-start"});
+    int rc = init.main(new String[] {
+        "-d", tmp.getPath(), "--batch", "--no-auto-start",
+        "--skip-plugins"});
     if (rc != 0) {
       throw new RuntimeException("Couldn't initialize site");
     }
-    return path;
+
+    MergeableFileBasedConfig cfg = new MergeableFileBasedConfig(
+        new File(new File(tmp, "etc"), "gerrit.config"),
+        FS.DETECTED);
+    cfg.load();
+    cfg.merge(base);
+    mergeTestConfig(cfg);
+    cfg.save();
+    return tmp;
+  }
+
+  private static void mergeTestConfig(Config cfg)
+      throws IOException {
+    InetSocketAddress http = newPort();
+    InetSocketAddress sshd = newPort();
+    String url = "http://" + format(http) + "/";
+
+    cfg.setString("gerrit", null, "canonicalWebUrl", url);
+    cfg.setString("httpd", null, "listenUrl", url);
+    cfg.setString("sshd", null, "listenAddress", format(sshd));
+    cfg.setString("cache", null, "directory", null);
+    cfg.setBoolean("sendemail", null, "enable", false);
+    cfg.setInt("cache", "projects", "checkFrequency", 0);
+    cfg.setInt("plugins", null, "checkFrequency", 0);
+  }
+
+  private static String format(InetSocketAddress s) {
+    return String.format("%s:%d", s.getAddress().getHostAddress(), s.getPort());
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
@@ -103,15 +152,63 @@
     return (T) f.get(obj);
   }
 
+  private static final InetSocketAddress newPort() throws IOException {
+    ServerSocket s = new ServerSocket(0, 0, getLocalHost());
+    try {
+      return (InetSocketAddress) s.getLocalSocketAddress();
+    } finally {
+      s.close();
+    }
+  }
+
+  private static InetAddress getLocalHost() throws UnknownHostException {
+    try {
+      return InetAddress.getLocalHost();
+    } catch (UnknownHostException e1) {
+      try {
+        return InetAddress.getByName("localhost");
+      } catch (UnknownHostException e2) {
+        return InetAddress.getByName("127.0.0.1");
+      }
+    }
+  }
+
+  private File sitePath;
   private Daemon daemon;
   private ExecutorService daemonService;
   private Injector testInjector;
+  private String url;
+  private InetSocketAddress sshdAddress;
+  private InetSocketAddress httpAddress;
 
-  private GerritServer(Injector testInjector,
-      Daemon daemon, ExecutorService daemonService) {
+  private GerritServer(File sitePath, Injector testInjector, Daemon daemon,
+      ExecutorService daemonService) throws IOException, ConfigInvalidException {
+    this.sitePath = sitePath;
     this.testInjector = testInjector;
     this.daemon = daemon;
     this.daemonService = daemonService;
+
+    Config cfg = testInjector.getInstance(
+      Key.get(Config.class, GerritServerConfig.class));
+    url = cfg.getString("gerrit", null, "canonicalWebUrl");
+    URI uri = URI.create(url);
+
+    sshdAddress = SocketUtil.resolve(
+        cfg.getString("sshd", null, "listenAddress"),
+        0);
+    httpAddress = new InetSocketAddress(uri.getHost(), uri.getPort());
+  }
+
+  String getUrl() {
+    return url;
+  }
+
+  InetSocketAddress getSshdAddress() {
+    return sshdAddress;
+  }
+
+  InetSocketAddress getHttpAddress() {
+    return httpAddress;
   }
 
   Injector getTestInjector() {
@@ -119,10 +216,15 @@
   }
 
   void stop() throws Exception {
-    LifecycleManager manager = get(daemon, "manager");
-    System.out.println("Gerrit Server Shutdown");
-    manager.stop();
-    daemonService.shutdownNow();
-    daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+    daemon.getLifecycleManager().stop();
+    if (daemonService != null) {
+      System.out.println("Gerrit Server Shutdown");
+      daemonService.shutdownNow();
+      daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+    }
+    if (sitePath != null) {
+      TempFileUtil.recursivelyDelete(sitePath);
+    }
+    RepositoryCache.clear();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
new file mode 100644
index 0000000..5576c4f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.config.TrackingFootersProvider;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.schema.DataSourceType;
+import com.google.gerrit.server.schema.SchemaModule;
+import com.google.gerrit.server.schema.SchemaVersion;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryH2Type;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import org.apache.sshd.common.KeyPairProvider;
+import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+
+class InMemoryTestingDatabaseModule extends AbstractModule {
+  private final Config cfg;
+
+  InMemoryTestingDatabaseModule(Config cfg) {
+    this.cfg = cfg;
+  }
+
+  @Override
+  protected void configure() {
+    bind(Config.class)
+      .annotatedWith(GerritServerConfig.class)
+      .toInstance(cfg);
+
+    bind(File.class)
+      .annotatedWith(SitePath.class)
+      .toInstance(new File("UNIT_TEST_GERRIT_SITE"));
+
+    bind(GitRepositoryManager.class)
+      .toInstance(new InMemoryRepositoryManager());
+
+    bind(DataSourceType.class).to(InMemoryH2Type.class);
+    bind(InMemoryDatabase.class).in(SINGLETON);
+    bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {})
+      .to(InMemoryDatabase.class);
+
+    install(new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().to(CreateDatabase.class);
+      }
+    });
+
+    bind(SitePaths.class);
+    bind(TrackingFooters.class)
+      .toProvider(TrackingFootersProvider.class)
+      .in(SINGLETON);
+
+    install(new SchemaModule());
+    bind(SchemaVersion.class).to(SchemaVersion.C);
+  }
+
+  @Provides
+  @Singleton
+  KeyPairProvider createHostKey() {
+    return getHostKeys();
+  }
+
+  private static SimpleGeneratorHostKeyProvider keys;
+
+  private static synchronized KeyPairProvider getHostKeys() {
+    if (keys == null) {
+      keys = new SimpleGeneratorHostKeyProvider();
+      keys.setAlgorithm("RSA");
+      keys.loadKeys();
+    }
+    return keys;
+  }
+
+  static class CreateDatabase implements LifecycleListener {
+    private final InMemoryDatabase mem;
+
+    @Inject
+    CreateDatabase(InMemoryDatabase mem) {
+      this.mem = mem;
+    }
+
+    @Override
+    public void start() {
+      try {
+        mem.create();
+      } catch (OrmException e) {
+        throw new OrmRuntimeException(e);
+      }
+    }
+
+    @Override
+    public void stop() {
+      mem.drop();
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
new file mode 100644
index 0000000..f1baa9d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.common.collect.Lists;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+
+/**
+ * A file based Config that can merge another Config instance.
+ */
+public class MergeableFileBasedConfig extends FileBasedConfig {
+  public MergeableFileBasedConfig(File cfgLocation, FS fs) {
+    super(cfgLocation, fs);
+  }
+
+  /**
+   * Merge another Config into this Config.
+   *
+   * In case a configuration parameter exists both in this instance and in the
+   * merged instance then the value in this instance will simply replaced by
+   * the value from the merged instance.
+   *
+   * @param s Config to merge into this instance
+   */
+  public void merge(Config s) {
+    if (s == null) {
+      return;
+    }
+    for (String section : s.getSections()) {
+      for (String subsection : s.getSubsections(section)) {
+        for (String name : s.getNames(section, subsection)) {
+          setStringList(section, subsection, name, Lists.newArrayList(s
+              .getStringList(section, subsection, name)));
+        }
+      }
+
+      for (String name : s.getNames(section)) {
+        setStringList(section, null, name,
+            Lists.newArrayList(s.getStringList(section, null, name)));
+      }
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
index 9f93489..9e5d702 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
@@ -16,11 +16,16 @@
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
 
+import com.google.common.base.Preconditions;
+
 import org.apache.http.HttpResponse;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.nio.ByteBuffer;
 
 public class RestResponse {
 
@@ -49,4 +54,19 @@
   public int getStatusCode() {
     return response.getStatusLine().getStatusCode();
   }
+
+  public String getEntityContent() throws IOException {
+    Preconditions.checkNotNull(response,
+        "Response is not initialized.");
+    Preconditions.checkNotNull(response.getEntity(),
+        "Response.Entity is not initialized.");
+      ByteBuffer buf = IO.readWholeStream(
+          response.getEntity().getContent(),
+          1024);
+      return RawParseUtils.decode(
+          buf.array(),
+          buf.arrayOffset(),
+          buf.limit())
+          .trim();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 2bf6523..9132be8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Charsets;
 import com.google.gson.Gson;
 
 import org.apache.http.auth.AuthScope;
@@ -25,21 +27,23 @@
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.message.BasicHeader;
-import org.apache.http.protocol.HTTP;
 
 import java.io.IOException;
+import java.net.URI;
 
 public class RestSession {
 
   private final TestAccount account;
+  private final String url;
   DefaultHttpClient client;
 
-  public RestSession(TestAccount account) {
+  public RestSession(GerritServer server, TestAccount account) {
+    this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
     this.account = account;
   }
 
   public RestResponse get(String endPoint) throws IOException {
-    HttpGet get = new HttpGet("http://localhost:8080/a" + endPoint);
+    HttpGet get = new HttpGet(url + "/a" + endPoint);
     return new RestResponse(getClient().execute(get));
   }
 
@@ -48,10 +52,12 @@
   }
 
   public RestResponse put(String endPoint, Object content) throws IOException {
-    HttpPut put = new HttpPut("http://localhost:8080/a" + endPoint);
+    HttpPut put = new HttpPut(url + "/a" + endPoint);
     if (content != null) {
       put.addHeader(new BasicHeader("Content-Type", "application/json"));
-      put.setEntity(new StringEntity((new Gson()).toJson(content), HTTP.UTF_8));
+      put.setEntity(new StringEntity(
+          new Gson().toJson(content),
+          Charsets.UTF_8.name()));
     }
     return new RestResponse(getClient().execute(put));
   }
@@ -61,24 +67,27 @@
   }
 
   public RestResponse post(String endPoint, Object content) throws IOException {
-    HttpPost post = new HttpPost("http://localhost:8080/a" + endPoint);
+    HttpPost post = new HttpPost(url + "/a" + endPoint);
     if (content != null) {
       post.addHeader(new BasicHeader("Content-Type", "application/json"));
-      post.setEntity(new StringEntity((new Gson()).toJson(content), HTTP.UTF_8));
+      post.setEntity(new StringEntity(
+          new Gson().toJson(content),
+          Charsets.UTF_8.name()));
     }
     return new RestResponse(getClient().execute(post));
   }
 
   public RestResponse delete(String endPoint) throws IOException {
-    HttpDelete delete = new HttpDelete("http://localhost:8080/a" + endPoint);
+    HttpDelete delete = new HttpDelete(url + "/a" + endPoint);
     return new RestResponse(getClient().execute(delete));
   }
 
   private DefaultHttpClient getClient() {
     if (client == null) {
+      URI uri = URI.create(url);
       client = new DefaultHttpClient();
       client.getCredentialsProvider().setCredentials(
-          new AuthScope("localhost", 8080),
+          new AuthScope(uri.getHost(), uri.getPort()),
           new UsernamePasswordCredentials(account.username, account.httpPassword));
     }
     return client;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
index aae9236..a150eba 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -16,6 +16,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.InetSocketAddress;
 import java.util.Scanner;
 
 import com.jcraft.jsch.ChannelExec;
@@ -25,11 +26,13 @@
 
 public class SshSession {
 
+  private final InetSocketAddress addr;
   private final TestAccount account;
   private Session session;
   private String error;
 
-  public SshSession(TestAccount account) {
+  public SshSession(GerritServer server, TestAccount account) {
+    this.addr = server.getSshdAddress();
     this.account = account;
   }
 
@@ -42,7 +45,7 @@
       channel.connect();
 
       Scanner s = new Scanner(channel.getErrStream()).useDelimiter("\\A");
-      error =  s.hasNext() ? s.next() : null;
+      error = s.hasNext() ? s.next() : null;
 
       s = new Scanner(in).useDelimiter("\\A");
       return s.hasNext() ? s.next() : "";
@@ -71,7 +74,10 @@
       JSch jsch = new JSch();
       jsch.addIdentity("KeyPair",
           account.privateKey(), account.sshKey.getPublicKeyBlob(), null);
-      session = jsch.getSession(account.username, "localhost", 29418);
+      session = jsch.getSession(
+          account.username,
+          addr.getAddress().getHostAddress(),
+          addr.getPort());
       session.setConfig("StrictHostKeyChecking", "no");
       session.connect();
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java
deleted file mode 100644
index 6ee2045..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance;
-
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.acceptance.rest.group.GroupInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * An example test that tests presence of system groups in a newly initialized
- * review site.
- *
- * The test shows how to perform these checks via SSH, REST or using Gerrit
- * internals.
- */
-public class SystemGroupsIT extends AbstractDaemonTest {
-
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private AccountCreator accounts;
-
-  protected TestAccount admin;
-
-  @Before
-  public void setUp() throws Exception {
-    admin = accounts.create("admin", "admin@sap.com", "Administrator",
-            "Administrators");
-  }
-
-  @Test
-  public void systemGroupsCreated_ssh() throws JSchException, IOException {
-    SshSession session = new SshSession(admin);
-    String result = session.exec("gerrit ls-groups");
-    assertTrue(result.contains("Administrators"));
-    assertTrue(result.contains("Anonymous Users"));
-    assertTrue(result.contains("Non-Interactive Users"));
-    assertTrue(result.contains("Project Owners"));
-    assertTrue(result.contains("Registered Users"));
-    session.close();
-  }
-
-  @Test
-  public void systemGroupsCreated_rest() throws IOException {
-    RestSession session = new RestSession(admin);
-    RestResponse r = session.get("/groups/");
-    Gson gson = new Gson();
-    Map<String, GroupInfo> result =
-        gson.fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
-    Set<String> names = result.keySet();
-    assertTrue(names.contains("Administrators"));
-    assertTrue(names.contains("Anonymous Users"));
-    assertTrue(names.contains("Non-Interactive Users"));
-    assertTrue(names.contains("Project Owners"));
-    assertTrue(names.contains("Registered Users"));
-  }
-
-  @Test
-  public void systemGroupsCreated_internals() throws OrmException {
-    ReviewDb db = reviewDbProvider.open();
-    try {
-      Set<String> names = Sets.newHashSet();
-      for (AccountGroup g : db.accountGroups().all()) {
-        names.add(g.getName());
-      }
-      assertTrue(names.contains("Administrators"));
-      assertTrue(names.contains("Anonymous Users"));
-      assertTrue(names.contains("Non-Interactive Users"));
-      assertTrue(names.contains("Project Owners"));
-      assertTrue(names.contains("Registered Users"));
-    } finally {
-      db.close();
-    }
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
index adee361..ff0ca7b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
@@ -15,27 +15,41 @@
 package com.google.gerrit.acceptance;
 
 import java.io.File;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.io.IOException;
 
 public class TempFileUtil {
-
-  private static int testCount;
-  private static DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
-  private static final File temp = new File(new File("target"), "temp");
-
-  private static String createUniqueTestFolderName() {
-    return "test_" + (df.format(new Date()) + "_" + (testCount++));
+  public static File createTempDirectory() throws IOException {
+    File tmp = File.createTempFile("gerrit_test_", "");
+    if (!tmp.delete() || !tmp.mkdir()) {
+      throw new IOException("Cannot create " + tmp.getPath());
+    }
+    return tmp;
   }
 
-  public static File createTempDirectory() {
-    final String name = createUniqueTestFolderName();
-    final File directory = new File(temp, name);
-    if (!directory.mkdirs()) {
-      throw new RuntimeException("failed to create folder '"
-          + directory.getAbsolutePath() + "'");
+  public static void recursivelyDelete(File dir) throws IOException {
+    if (!dir.getPath().equals(dir.getCanonicalPath())) {
+      // Directory symlink reaching outside of temporary space.
+      return;
     }
-    return directory;
+    File[] contents = dir.listFiles();
+    if (contents != null) {
+      for (File d : contents) {
+        if (d.isDirectory()) {
+          recursivelyDelete(d);
+        } else {
+          deleteNowOrOnExit(d);
+        }
+      }
+    }
+    deleteNowOrOnExit(dir);
+  }
+
+  private static void deleteNowOrOnExit(File dir) {
+    if (!dir.delete()) {
+      dir.deleteOnExit();
+    }
+  }
+
+  private TempFileUtil() {
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index 358680f..b85ffea 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -51,13 +51,11 @@
     return new PersonIdent(username, email);
   }
 
-  public String getHttpUrl() {
-    StringBuilder b = new StringBuilder();
-    b.append("http://");
-    b.append(username);
-    b.append(":");
-    b.append(httpPassword);
-    b.append("@localhost:8080");
-    return b.toString();
+  public String getHttpUrl(GerritServer server) {
+    return String.format("http://%s:%s@%s:%d",
+        username,
+        httpPassword,
+        server.getHttpAddress().getAddress().getHostAddress(),
+        server.getHttpAddress().getPort());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
new file mode 100644
index 0000000..0931e12
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class UseGerritConfigAnnotationTest extends AbstractDaemonTest {
+
+  @Inject
+  @GerritServerConfig
+  Config serverConfig;
+
+  @Test
+  @GerritConfig(name="x.y", value="z")
+  public void testOne() {
+    assertEquals("z", serverConfig.getString("x", null, "y"));
+  }
+
+  @Test
+  @GerritConfigs({
+      @GerritConfig(name="x.y", value="z"),
+      @GerritConfig(name="a.b", value="c"),
+  })
+  public void testMultiple() {
+    assertEquals("z", serverConfig.getString("x", null, "y"));
+    assertEquals("c", serverConfig.getString("a", null, "b"));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
new file mode 100644
index 0000000..f9367ec
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+public @interface UseLocalDisk {
+}
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
new file mode 100644
index 0000000..a6ce132
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -0,0 +1,196 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public abstract class AbstractPushForReview extends AbstractDaemonTest {
+  protected enum Protocol {
+    SSH, HTTP
+  }
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+  private Project.NameKey project;
+  private Git git;
+  private ReviewDb db;
+  private String sshUrl;
+
+  @Before
+  public void setUp() throws Exception {
+    admin =
+        accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+
+    project = new Project.NameKey("p");
+    initSsh(admin);
+    SshSession sshSession = new SshSession(server, admin);
+    createProject(sshSession, project.get());
+    sshUrl = sshSession.getUrl();
+    sshSession.close();
+
+    db = reviewDbProvider.open();
+  }
+
+  protected void selectProtocol(Protocol p) throws GitAPIException, IOException {
+    String url;
+    switch (p) {
+      case SSH:
+        url = sshUrl;
+        break;
+      case HTTP:
+        url = admin.getHttpUrl(server);
+        break;
+      default:
+        throw new IllegalArgumentException("unexpected protocol: " + p);
+    }
+    git = cloneProject(url + "/" + project.get());
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void testPushForMaster() throws GitAPIException, OrmException,
+      IOException {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, null);
+  }
+
+  @Test
+  public void testPushForMasterWithTopic() throws GitAPIException,
+      OrmException, IOException {
+    // specify topic in ref
+    String topic = "my/topic";
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
+
+    // specify topic as option
+    r = pushTo("refs/for/master%topic=" + topic);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
+  }
+
+  @Test
+  public void testPushForMasterWithCc() throws GitAPIException, OrmException,
+      IOException, JSchException {
+    // cc one user
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    String topic = "my/topic";
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
+
+    // cc several users
+    TestAccount user2 =
+        accounts.create("another-user", "another.user@example.com", "Another User");
+    r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+        + user.email + ",cc=" + user2.email);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
+
+    // cc non-existing user
+    String nonExistingEmail = "non.existing@example.com";
+    r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+        + nonExistingEmail + ",cc=" + user.email);
+    r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
+  }
+
+  @Test
+  public void testPushForMasterWithReviewer() throws GitAPIException,
+      OrmException, IOException, JSchException {
+    // add one reviewer
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    String topic = "my/topic";
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic, user);
+
+    // add several reviewers
+    TestAccount user2 =
+        accounts.create("another-user", "another.user@example.com", "Another User");
+    r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
+        + ",r=" + user2.email);
+    r.assertOkStatus();
+    // admin is the owner of the change and should not appear as reviewer
+    r.assertChange(Change.Status.NEW, topic, user, user2);
+
+    // add non-existing user as reviewer
+    String nonExistingEmail = "non.existing@example.com";
+    r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r="
+        + nonExistingEmail + ",r=" + user.email);
+    r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
+  }
+
+  @Test
+  public void testPushForMasterAsDraft() throws GitAPIException, OrmException,
+      IOException {
+    // create draft by pushing to 'refs/drafts/'
+    PushOneCommit.Result r = pushTo("refs/drafts/master");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.DRAFT, null);
+
+    // create draft by using 'draft' option
+    r = pushTo("refs/for/master%draft");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.DRAFT, null);
+  }
+
+  @Test
+  public void testPushForNonExistingBranch() throws GitAPIException,
+      OrmException, IOException {
+    String branchName = "non-existing";
+    PushOneCommit.Result r = pushTo("refs/for/" + branchName);
+    r.assertErrorStatus("branch " + branchName + " not found");
+  }
+
+  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, ref);
+  }
+}
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
new file mode 100644
index 0000000..6014118
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
@@ -0,0 +1,38 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = ['SubmitOnPushIT.java'],
+  deps = [':util'],
+)
+
+acceptance_tests(
+  srcs = ['HttpPushForReviewIT.java', 'SshPushForReviewIT.java'],
+  deps = [':push_for_review'],
+)
+
+java_library(
+  name = 'push_for_review',
+  srcs = ['AbstractPushForReview.java'],
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests:lib',
+  ],
+)
+
+java_library(
+  name = 'util',
+  srcs = [
+    'GitUtil.java',
+    'PushOneCommit.java',
+  ],
+  deps = [
+    '//gerrit-acceptance-tests:lib',
+    '//gerrit-reviewdb:server',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:jsch',
+    '//lib/jgit:jgit',
+    '//lib:junit',
+  ],
+  visibility = ['//gerrit-acceptance-tests/...'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
index 045207c..c17598f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
@@ -18,12 +18,14 @@
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.TempFileUtil;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
 
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.Session;
 
 import org.eclipse.jgit.api.AddCommand;
+import org.eclipse.jgit.api.CheckoutCommand;
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.CommitCommand;
 import org.eclipse.jgit.api.Git;
@@ -32,6 +34,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.JschConfigSessionFactory;
@@ -74,10 +77,34 @@
 
   public static void createProject(SshSession s, String name)
       throws JSchException, IOException {
-    s.exec("gerrit create-project --empty-commit --name \"" + name + "\"");
+    createProject(s, name, null);
   }
 
-  public static Git cloneProject(String url) throws GitAPIException {
+  public static void createProject(SshSession s, String name, Project.NameKey parent)
+      throws JSchException, IOException {
+    createProject(s, name, parent, true);
+  }
+
+  public static void createProject(SshSession s, String name,
+      Project.NameKey parent, boolean emptyCommit)
+      throws JSchException, IOException {
+    StringBuilder b = new StringBuilder();
+    b.append("gerrit create-project");
+    if (emptyCommit) {
+      b.append(" --empty-commit");
+    }
+    b.append(" --name \"");
+    b.append(name);
+    b.append("\"");
+    if (parent != null) {
+      b.append(" --parent \"");
+      b.append(parent.get());
+      b.append("\"");
+    }
+    s.exec(b.toString());
+  }
+
+  public static Git cloneProject(String url) throws GitAPIException, IOException {
     final File gitDir = TempFileUtil.createTempDirectory();
     final CloneCommand cloneCmd = Git.cloneRepository();
     cloneCmd.setURI(url);
@@ -105,47 +132,58 @@
     addCmd.call();
   }
 
-  public static String createCommit(Git git, PersonIdent i, String msg)
+  public static Commit createCommit(Git git, PersonIdent i, String msg)
       throws GitAPIException, IOException {
-    return createCommit(git, i, msg, true, false);
+    return createCommit(git, i, msg, null);
   }
 
-  public static void amendCommit(Git git, PersonIdent i, String msg, String changeId)
+  public static Commit amendCommit(Git git, PersonIdent i, String msg, String changeId)
       throws GitAPIException, IOException {
     msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
-    createCommit(git, i, msg, false, true);
+    return createCommit(git, i, msg, changeId);
   }
 
-  private static String createCommit(Git git, PersonIdent i, String msg,
-      boolean insertChangeId, boolean amend) throws GitAPIException, IOException {
-    ObjectId changeId = null;
-    if (insertChangeId) {
-      changeId = computeChangeId(git, i, msg);
-      msg = ChangeIdUtil.insertId(msg, changeId);
-    }
+  private static Commit createCommit(Git git, PersonIdent i, String msg,
+      String changeId) throws GitAPIException, IOException {
 
     final CommitCommand commitCmd = git.commit();
-    commitCmd.setAmend(amend);
+    commitCmd.setAmend(changeId != null);
     commitCmd.setAuthor(i);
     commitCmd.setCommitter(i);
-    commitCmd.setMessage(msg);
-    commitCmd.call();
 
-    return changeId != null ? "I" + changeId.getName() : null;
+    if (changeId == null) {
+      ObjectId id = computeChangeId(git, i, msg);
+      changeId = "I" + id.getName();
+    }
+    msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
+    commitCmd.setMessage(msg);
+
+    RevCommit c = commitCmd.call();
+    return new Commit(c, changeId);
   }
 
   private static ObjectId computeChangeId(Git git, PersonIdent i, String msg)
       throws IOException {
     RevWalk rw = new RevWalk(git.getRepository());
     try {
-      RevCommit parent =
-          rw.lookupCommit(git.getRepository().getRef(Constants.HEAD).getObjectId());
-      return ChangeIdUtil.computeChangeId(parent.getTree(), parent.getId(), i, i, msg);
+      Ref head = git.getRepository().getRef(Constants.HEAD);
+      if (head.getObjectId() != null) {
+        RevCommit parent = rw.lookupCommit(head.getObjectId());
+        return ChangeIdUtil.computeChangeId(parent.getTree(), parent.getId(), i, i, msg);
+      } else {
+        return ChangeIdUtil.computeChangeId(null, null, i, i, msg);
+      }
     } finally {
       rw.release();
     }
   }
 
+  public static void checkout(Git git, String name) throws GitAPIException {
+    CheckoutCommand checkout = git.checkout();
+    checkout.setName(name);
+    checkout.call();
+  }
+
   public static PushResult pushHead(Git git, String ref, boolean pushTags)
       throws GitAPIException {
     PushCommand pushCmd = git.push();
@@ -156,4 +194,22 @@
     Iterable<PushResult> r = pushCmd.call();
     return Iterables.getOnlyElement(r);
   }
+
+  public static class Commit {
+    private final RevCommit commit;
+    private final String changeId;
+
+    Commit(RevCommit commit, String changeId) {
+      this.commit = commit;
+      this.changeId = changeId;
+    }
+
+    public RevCommit getCommit() {
+      return commit;
+    }
+
+    public String getChangeId() {
+      return changeId;
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
new file mode 100644
index 0000000..465befd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.Before;
+
+import java.io.IOException;
+
+public class HttpPushForReviewIT extends AbstractPushForReview {
+  @Before
+  public void selectHttpUrl() throws GitAPIException, IOException {
+    selectProtocol(Protocol.HTTP);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
deleted file mode 100644
index 9799cc1..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.git;
-
-import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.git.GitUtil.createProject;
-import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AccountCreator;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-
-public class PushForReviewIT extends AbstractDaemonTest {
-  private enum Protocol {
-    SSH, HTTP
-  }
-
-  @Inject
-  private AccountCreator accounts;
-
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  private TestAccount admin;
-  private Project.NameKey project;
-  private Git git;
-  private ReviewDb db;
-  private String sshUrl;
-
-  @Before
-  public void setUp() throws Exception {
-    admin =
-        accounts.create("admin", "admin@example.com", "Administrator",
-            "Administrators");
-
-    project = new Project.NameKey("p");
-    initSsh(admin);
-    SshSession sshSession = new SshSession(admin);
-    createProject(sshSession, project.get());
-    sshUrl = sshSession.getUrl();
-    sshSession.close();
-
-    db = reviewDbProvider.open();
-  }
-
-  private void selectProtocol(Protocol p) throws GitAPIException, IOException {
-    String url;
-    switch (p) {
-      case SSH:
-        url = sshUrl;
-        break;
-      case HTTP:
-        url = admin.getHttpUrl();
-        break;
-      default:
-        throw new IllegalArgumentException("unexpected protocol: " + p);
-    }
-    git = cloneProject(url + "/" + project.get());
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
-  @Test
-  public void testPushForMaster_HTTP() throws GitAPIException, OrmException,
-      IOException {
-    testPushForMaster(Protocol.HTTP);
-  }
-
-  @Test
-  public void testPushForMaster_SSH() throws GitAPIException, OrmException,
-      IOException {
-    testPushForMaster(Protocol.SSH);
-  }
-
-  private void testPushForMaster(Protocol p) throws GitAPIException,
-      OrmException, IOException {
-    selectProtocol(p);
-    PushOneCommit.Result r = pushTo("refs/for/master");
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, null);
-  }
-
-  @Test
-  public void testPushForMasterWithTopic_HTTP()
-      throws GitAPIException, OrmException, IOException {
-    testPushForMasterWithTopic(Protocol.HTTP);
-  }
-
-  @Test
-  public void testPushForMasterWithTopic_SSH()
-      throws GitAPIException, OrmException, IOException {
-    testPushForMasterWithTopic(Protocol.SSH);
-  }
-
-  private void testPushForMasterWithTopic(Protocol p) throws GitAPIException,
-      OrmException, IOException {
-    selectProtocol(p);
-    // specify topic in ref
-    String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic);
-
-    // specify topic as option
-    r = pushTo("refs/for/master%topic=" + topic);
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic);
-  }
-
-  @Test
-  public void testPushForMasterWithCc_HTTP() throws GitAPIException,
-      OrmException, IOException, JSchException {
-    testPushForMasterWithCc(Protocol.HTTP);
-  }
-
-  @Test
-  public void testPushForMasterWithCc_SSH() throws GitAPIException,
-      OrmException, IOException, JSchException {
-    testPushForMasterWithCc(Protocol.SSH);
-  }
-
-  private void testPushForMasterWithCc(Protocol p) throws GitAPIException,
-      OrmException, IOException, JSchException {
-    selectProtocol(p);
-    // cc one user
-    TestAccount user = accounts.create("user", "user@example.com", "User");
-    String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic);
-
-    // cc several users
-    TestAccount user2 =
-        accounts.create("another-user", "another.user@example.com", "Another User");
-    r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
-        + user.email + ",cc=" + user2.email);
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic);
-
-    // cc non-existing user
-    String nonExistingEmail = "non.existing@example.com";
-    r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
-        + nonExistingEmail + ",cc=" + user.email);
-    r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
-  }
-
-  @Test
-  public void testPushForMasterWithReviewer_HTTP() throws GitAPIException,
-      OrmException, IOException, JSchException {
-    testPushForMasterWithReviewer(Protocol.HTTP);
-  }
-
-  @Test
-  public void testPushForMasterWithReviewer_SSH() throws GitAPIException,
-      OrmException, IOException, JSchException {
-    testPushForMasterWithReviewer(Protocol.SSH);
-  }
-
-  private void testPushForMasterWithReviewer(Protocol p)
-      throws GitAPIException, OrmException, IOException, JSchException {
-    selectProtocol(p);
-    // add one reviewer
-    TestAccount user = accounts.create("user", "user@example.com", "User");
-    String topic = "my/topic";
-    PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic, user);
-
-    // add several reviewers
-    TestAccount user2 =
-        accounts.create("another-user", "another.user@example.com", "Another User");
-    r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
-        + ",r=" + user2.email);
-    r.assertOkStatus();
-    // admin is the owner of the change and should not appear as reviewer
-    r.assertChange(Change.Status.NEW, topic, user, user2);
-
-    // add non-existing user as reviewer
-    String nonExistingEmail = "non.existing@example.com";
-    r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r="
-        + nonExistingEmail + ",r=" + user.email);
-    r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
-  }
-
-  @Test
-  public void testPushForMasterAsDraft_HTTP() throws GitAPIException,
-      OrmException, IOException {
-    testPushForMasterAsDraft(Protocol.HTTP);
-  }
-
-  @Test
-  public void testPushForMasterAsDraft_SSH() throws GitAPIException,
-      OrmException, IOException {
-    testPushForMasterAsDraft(Protocol.SSH);
-  }
-
-  private void testPushForMasterAsDraft(Protocol p) throws GitAPIException,
-      OrmException, IOException {
-    selectProtocol(p);
-    // create draft by pushing to 'refs/drafts/'
-    PushOneCommit.Result r = pushTo("refs/drafts/master");
-    r.assertOkStatus();
-    r.assertChange(Change.Status.DRAFT, null);
-
-    // create draft by using 'draft' option
-    r = pushTo("refs/for/master%draft");
-    r.assertOkStatus();
-    r.assertChange(Change.Status.DRAFT, null);
-  }
-
-  @Test
-  public void testPushForNonExistingBranch_HTTP() throws GitAPIException,
-      OrmException, IOException {
-    testPushForNonExistingBranch(Protocol.HTTP);
-  }
-
-  @Test
-  public void testPushForNonExistingBranch_SSH() throws GitAPIException,
-      OrmException, IOException {
-    testPushForNonExistingBranch(Protocol.SSH);
-  }
-
-  private void testPushForNonExistingBranch(Protocol p) throws GitAPIException,
-      OrmException, IOException {
-    selectProtocol(p);
-    String branchName = "non-existing";
-    PushOneCommit.Result r = pushTo("refs/for/" + branchName);
-    r.assertErrorStatus("branch " + branchName + " not found");
-  }
-
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
-    return push.to(git, ref);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
index 4c32f2f..1c0fafe 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.GitUtil.Commit;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -36,7 +37,9 @@
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
@@ -46,10 +49,10 @@
 import java.util.Set;
 
 public class PushOneCommit {
-  public final static String SUBJECT = "test commit";
+  public static final String SUBJECT = "test commit";
 
-  private final static String FILE_NAME = "a.txt";
-  private final static String FILE_CONTENT = "some content";
+  private static final String FILE_NAME = "a.txt";
+  private static final String FILE_CONTENT = "some content";
 
   private final ReviewDb db;
   private final PersonIdent i;
@@ -82,15 +85,17 @@
   public Result to(Git git, String ref)
       throws GitAPIException, IOException {
     add(git, fileName, content);
+    Commit c;
     if (changeId != null) {
-      amendCommit(git, i, subject, changeId);
+      c = amendCommit(git, i, subject, changeId);
     } else {
-      changeId = createCommit(git, i, subject);
+      c = createCommit(git, i, subject);
+      changeId = c.getChangeId();
     }
     if (tagName != null) {
       git.tag().setName(tagName).setAnnotated(false).call();
     }
-    return new Result(db, ref, pushHead(git, ref, tagName != null), changeId, subject);
+    return new Result(db, ref, pushHead(git, ref, tagName != null), c, subject);
   }
 
   public void setTag(final String tagName) {
@@ -101,32 +106,40 @@
     private final ReviewDb db;
     private final String ref;
     private final PushResult result;
-    private final String changeId;
+    private final Commit commit;
     private final String subject;
 
-    private Result(ReviewDb db, String ref, PushResult result, String changeId,
+    private Result(ReviewDb db, String ref, PushResult result, Commit commit,
         String subject) {
       this.db = db;
       this.ref = ref;
       this.result = result;
-      this.changeId = changeId;
+      this.commit = commit;
       this.subject = subject;
     }
 
     public PatchSet.Id getPatchSetId() throws OrmException {
       return Iterables.getOnlyElement(
-          db.changes().byKey(new Change.Key(changeId))).currentPatchSetId();
+          db.changes().byKey(new Change.Key(commit.getChangeId()))).currentPatchSetId();
     }
 
     public String getChangeId() {
-      return changeId;
+      return commit.getChangeId();
+    }
+
+    public ObjectId getCommitId() {
+      return commit.getCommit().getId();
+    }
+
+    public RevCommit getCommit() {
+      return commit.getCommit();
     }
 
     public void assertChange(Change.Status expectedStatus,
         String expectedTopic, TestAccount... expectedReviewers)
         throws OrmException {
       Change c =
-          Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
+          Iterables.getOnlyElement(db.changes().byKey(new Change.Key(commit.getChangeId())).toList());
       assertEquals(subject, c.getSubject());
       assertEquals(expectedStatus, c.getStatus());
       assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
new file mode 100644
index 0000000..5251d2d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.Before;
+
+import java.io.IOException;
+
+public class SshPushForReviewIT extends AbstractPushForReview {
+  @Before
+  public void selectSshUrl() throws GitAPIException, IOException {
+    selectProtocol(Protocol.SSH);
+  }
+}
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 ab97d19..baeafe1 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
@@ -97,7 +97,7 @@
 
     project = new Project.NameKey("p");
     initSsh(admin);
-    SshSession sshSession = new SshSession(admin);
+    SshSession sshSession = new SshSession(server, admin);
     createProject(sshSession, project.get());
     git = cloneProject(sshSession.getUrl() + "/" + project.get());
     sshSession.close();
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
new file mode 100644
index 0000000..1fca451
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
@@ -0,0 +1,22 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change:util',
+  ],
+)
+
+java_library(
+  name = 'util',
+  srcs = ['AccountAssert.java', 'AccountInfo.java'],
+  deps = [
+    '//gerrit-acceptance-tests:lib',
+    '//gerrit-reviewdb:server',
+    '//lib:gwtorm',
+    '//lib:junit',
+  ],
+  visibility = ['//gerrit-acceptance-tests/...'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index 8d75487..3e8183e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -45,7 +45,7 @@
   public void setUp() throws Exception {
     admin = accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
new file mode 100644
index 0000000..b5ae7de
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.acceptance.git.PushOneCommit.Result;
+import com.google.gerrit.acceptance.rest.change.ChangeInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class StarredChangesIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+
+  private RestSession session;
+  private Git git;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.admin();
+    session = new RestSession(server, admin);
+    initSsh(admin);
+    Project.NameKey project = new Project.NameKey("p");
+    SshSession sshSession = new SshSession(server, admin);
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
+    sshSession.close();
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void starredChangeState() throws GitAPIException, IOException,
+      OrmException {
+    Result c1 = createChange();
+    Result c2 = createChange();
+    assertNull(getChange(c1.getChangeId()).starred);
+    assertNull(getChange(c2.getChangeId()).starred);
+    starChange(true, c1.getPatchSetId().getParentKey());
+    starChange(true, c2.getPatchSetId().getParentKey());
+    assertTrue(getChange(c1.getChangeId()).starred);
+    assertTrue(getChange(c2.getChangeId()).starred);
+    starChange(false, c1.getPatchSetId().getParentKey());
+    starChange(false, c2.getPatchSetId().getParentKey());
+    assertNull(getChange(c1.getChangeId()).starred);
+    assertNull(getChange(c2.getChangeId()).starred);
+  }
+
+  private ChangeInfo getChange(String changeId) throws IOException {
+    RestResponse r = session.get("/changes/?q=" + changeId);
+    List<ChangeInfo> c = (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<ChangeInfo>>() {}.getType());
+    return c.get(0);
+  }
+
+  private void starChange(boolean on, Change.Id id) throws IOException {
+    String url = "/accounts/self/starred.changes/" + id.get();
+    if (on) {
+      RestResponse r = session.put(url);
+      assertEquals(204, r.getStatusCode());
+    } else {
+      RestResponse r = session.delete(url);
+      assertEquals(204, r.getStatusCode());
+    }
+  }
+
+  private Result createChange() throws GitAPIException, IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, "refs/for/master");
+  }
+}
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
new file mode 100644
index 0000000..fc2fccd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -0,0 +1,257 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.GitUtil;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public abstract class AbstractSubmit extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  private GitRepositoryManager repoManager;
+
+  protected RestSession session;
+
+  private TestAccount admin;
+  private Project.NameKey project;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.admin();
+    session = new RestSession(server, admin);
+    initSsh(admin);
+
+    project = new Project.NameKey("p");
+
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  protected abstract SubmitType getSubmitType();
+
+  @Test
+  public void submitToEmptyRepo() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject(false);
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    assertEquals(change.getCommitId(), getRemoteHead().getId());
+  }
+
+  protected Git createProject() throws JSchException, IOException,
+      GitAPIException {
+    return createProject(true);
+  }
+
+  private Git createProject(boolean emptyCommit)
+      throws JSchException, IOException, GitAPIException {
+    SshSession sshSession = new SshSession(server, admin);
+    try {
+      GitUtil.createProject(sshSession, project.get(), null, emptyCommit);
+      setSubmitType(getSubmitType());
+      return cloneProject(sshSession.getUrl() + "/" + project.get());
+    } finally {
+      sshSession.close();
+    }
+  }
+
+  private void setSubmitType(SubmitType submitType) throws IOException {
+    ProjectConfigInput in = new ProjectConfigInput();
+    in.submit_type = submitType;
+    in.use_content_merge = InheritableBoolean.FALSE;
+    RestResponse r = session.put("/projects/" + project.get() + "/config", in);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+  }
+
+  protected void setUseContentMerge() throws IOException {
+    ProjectConfigInput in = new ProjectConfigInput();
+    in.use_content_merge = InheritableBoolean.TRUE;
+    RestResponse r = session.put("/projects/" + project.get() + "/config", in);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+  }
+
+  protected PushOneCommit.Result createChange(Git git) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, "refs/for/master");
+  }
+
+  protected PushOneCommit.Result createChange(Git git, String subject,
+      String fileName, String content) throws GitAPIException, IOException {
+    PushOneCommit push =
+        new PushOneCommit(db, admin.getIdent(), subject, fileName, content);
+    return push.to(git, "refs/for/master");
+  }
+
+  protected void submit(String changeId) throws IOException {
+    submit(changeId, HttpStatus.SC_OK);
+  }
+
+  protected void submitWithConflict(String changeId) throws IOException {
+    submit(changeId, HttpStatus.SC_CONFLICT);
+  }
+
+  private void submit(String changeId, int expectedStatus) throws IOException {
+    approve(changeId);
+    RestResponse r =
+        session.post("/changes/" + changeId + "/submit",
+            SubmitInput.waitForMerge());
+    assertEquals(expectedStatus, r.getStatusCode());
+    if (expectedStatus == HttpStatus.SC_OK) {
+      ChangeInfo change =
+          (new Gson()).fromJson(r.getReader(),
+              new TypeToken<ChangeInfo>() {}.getType());
+      assertEquals(Change.Status.MERGED, change.status);
+    }
+    r.consume();
+  }
+
+  private void approve(String changeId) throws IOException {
+    RestResponse r =
+        session.post("/changes/" + changeId + "/revisions/current/review",
+            ReviewInput.approve());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+  }
+
+  protected void assertCherryPick(Git localGit, boolean contentMerge) throws IOException {
+    assertRebase(localGit, contentMerge);
+    RevCommit remoteHead = getRemoteHead();
+    assertFalse(remoteHead.getFooterLines("Reviewed-On").isEmpty());
+    assertFalse(remoteHead.getFooterLines("Reviewed-By").isEmpty());
+  }
+
+  protected void assertRebase(Git localGit, boolean contentMerge) throws IOException {
+    Repository repo = localGit.getRepository();
+    RevCommit localHead = getHead(repo);
+    RevCommit remoteHead = getRemoteHead();
+    assertNotEquals(localHead.getId(), remoteHead.getId());
+    assertEquals(1, remoteHead.getParentCount());
+    if (!contentMerge) {
+      assertEquals(getLatestDiff(repo), getLatestRemoteDiff());
+    }
+    assertEquals(localHead.getShortMessage(), remoteHead.getShortMessage());
+  }
+
+  private RevCommit getHead(Repository repo) throws IOException {
+    return getHead(repo, "HEAD");
+  }
+
+  protected RevCommit getRemoteHead() throws IOException {
+    Repository repo = repoManager.openRepository(project);
+    try {
+      return getHead(repo, "refs/heads/master");
+    } finally {
+      repo.close();
+    }
+  }
+
+  private RevCommit getHead(Repository repo, String name) throws IOException {
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        return rw.parseCommit(repo.getRef(name).getObjectId());
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  private String getLatestDiff(Repository repo) throws IOException {
+    ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}");
+    ObjectId newTreeId = repo.resolve("HEAD^{tree}");
+    return getLatestDiff(repo, oldTreeId, newTreeId);
+  }
+
+  private String getLatestRemoteDiff() throws IOException {
+    Repository repo = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        ObjectId oldTreeId = repo.resolve("refs/heads/master~1^{tree}");
+        ObjectId newTreeId = repo.resolve("refs/heads/master^{tree}");
+        return getLatestDiff(repo, oldTreeId, newTreeId);
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  private String getLatestDiff(Repository repo, ObjectId oldTreeId,
+      ObjectId newTreeId) throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    DiffFormatter fmt = new DiffFormatter(out);
+    fmt.setRepository(repo);
+    fmt.format(oldTreeId, newTreeId);
+    fmt.flush();
+    return out.toString();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
new file mode 100644
index 0000000..770e554
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public abstract class AbstractSubmitByMerge extends AbstractSubmit {
+
+  @Test
+  public void submitWithMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submit(change2.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(2, head.getParentCount());
+    assertEquals(oldHead, head.getParent(0));
+    assertEquals(change2.getCommitId(), head.getParent(1));
+  }
+
+  @Test
+  public void submitWithContentMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+    submit(change.getChangeId());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+    submit(change2.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, change.getCommitId().getName());
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+    submit(change3.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(2, head.getParentCount());
+    assertEquals(oldHead, head.getParent(0));
+    assertEquals(change3.getCommitId(), head.getParent(1));
+  }
+
+  @Test
+  public void submitWithContentMerge_Conflict() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AccountInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AccountInfo.java
new file mode 100644
index 0000000..ee94476
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AccountInfo.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+public class AccountInfo {
+  public Integer _account_id;
+  public String name;
+  public String email;
+}
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
new file mode 100644
index 0000000..20b1033
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -0,0 +1,42 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = ['ChangeMessagesIT.java', 'DeleteDraftChangeIT.java',
+          'DeleteDraftPatchSetIT.java'],
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+  ],
+)
+
+acceptance_tests(
+  srcs = ['SubmitByCherryPickIT.java', 'SubmitByFastForwardIT.java',
+          'SubmitByMergeAlwaysIT.java', 'SubmitByMergeIfNecessaryIT.java',
+          'SubmitByRebaseIfNecessaryIT.java'],
+  deps = [
+    ':submit',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+  ],
+)
+
+java_library(
+  name = 'submit',
+  srcs = ['AbstractSubmit.java', 'AbstractSubmitByMerge.java'],
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests:lib',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+  ],
+)
+
+java_library(
+  name = 'util',
+  srcs = ['AccountInfo.java', 'ChangeInfo.java', 'ChangeMessageInfo.java',
+          'GroupInfo.java', 'ProjectConfigInput.java', 'ReviewInput.java',
+          'SubmitInput.java', 'SuggestReviewerInfo.java'],
+  deps = [
+    '//lib:guava',
+    '//gerrit-reviewdb:server',
+  ],
+  visibility = ['//gerrit-acceptance-tests/...'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java
new file mode 100644
index 0000000..8b431f0
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+
+import java.util.List;
+
+public class ChangeInfo {
+  String id;
+  String project;
+  String branch;
+  List<ChangeMessageInfo> messages;
+  Change.Status status;
+  public Boolean starred;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessageInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessageInfo.java
new file mode 100644
index 0000000..b3c584a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessageInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+public class ChangeMessageInfo {
+  String message;
+}
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
new file mode 100644
index 0000000..ec0e31f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -0,0 +1,152 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class ChangeMessagesIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+  private RestSession session;
+  private Git git;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(server, admin);
+    initSsh(admin);
+    Project.NameKey project = new Project.NameKey("p");
+    SshSession sshSession = new SshSession(server, admin);
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
+    sshSession.close();
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void messagesNotReturnedByDefault() throws GitAPIException,
+      IOException {
+    String changeId = createChange();
+    postMessage(changeId, "Some nits need to be fixed.");
+    ChangeInfo c = getChange(changeId);
+    assertNull(c.messages);
+  }
+
+  @Test
+  public void defaultMessage() throws GitAPIException,
+  IOException {
+    String changeId = createChange();
+    ChangeInfo c = getChangeWithMessages(changeId);
+    assertNotNull(c.messages);
+    assertEquals(1, c.messages.size());
+    assertEquals("Uploaded patch set 1.", c.messages.get(0).message);
+  }
+
+  @Test
+  public void messagesReturnedInChronologicalOrder() throws GitAPIException,
+      IOException {
+    String changeId = createChange();
+    String firstMessage = "Some nits need to be fixed.";
+    postMessage(changeId, firstMessage);
+    String secondMessage = "I like this feature.";
+    postMessage(changeId, secondMessage);
+    ChangeInfo c = getChangeWithMessages(changeId);
+    assertNotNull(c.messages);
+    assertEquals(3, c.messages.size());
+    assertEquals("Uploaded patch set 1.", c.messages.get(0).message);
+    assertMessage(firstMessage, c.messages.get(1).message);
+    assertMessage(secondMessage, c.messages.get(2).message);
+  }
+
+  private String createChange() throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, "refs/for/master").getChangeId();
+  }
+
+  private ChangeInfo getChange(String changeId) throws IOException {
+    return getChange(changeId, false);
+  }
+
+  private ChangeInfo getChangeWithMessages(String changeId) throws IOException {
+    return getChange(changeId, true);
+  }
+
+  private ChangeInfo getChange(String changeId, boolean includeMessages)
+      throws IOException {
+    RestResponse r =
+        session.get("/changes/?q=" + changeId
+            + (includeMessages ? "&o=MESSAGES" : ""));
+    List<ChangeInfo> c = (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<ChangeInfo>>() {}.getType());
+    return c.get(0);
+  }
+
+  private void assertMessage(String expected, String actual) {
+    assertEquals("Patch Set 1:\n\n" + expected, actual);
+  }
+
+  private void postMessage(String changeId, String msg) throws IOException {
+    ReviewInput in = new ReviewInput();
+    in.message = msg;
+    session.post("/changes/" + changeId + "/revisions/1/review", in).consume();
+  }
+
+  @SuppressWarnings("unused")
+  private class ReviewInput {
+    String message;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
new file mode 100644
index 0000000..7d4aa3e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class DeleteDraftChangeIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+
+  private RestSession session;
+  private Git git;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(server, admin);
+    initSsh(admin);
+    Project.NameKey project = new Project.NameKey("p");
+    SshSession sshSession = new SshSession(server, admin);
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
+    sshSession.close();
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void deleteChange() throws GitAPIException,
+      IOException {
+    String changeId = createChange();
+    ChangeInfo c = getChange(changeId);
+    assertEquals("p~master~" + changeId, c.id);
+    assertEquals(Change.Status.NEW, c.status);
+    RestResponse r = deleteChange(changeId, session);
+    assertEquals("Change is not a draft", r.getEntityContent());
+    assertEquals(409, r.getStatusCode());
+  }
+
+  @Test
+  public void deleteDraftChange() throws GitAPIException,
+      IOException, OrmException {
+    String changeId = createDraftChange();
+    ChangeInfo c = getChange(changeId);
+    assertEquals("p~master~" + changeId, c.id);
+    assertEquals(Change.Status.DRAFT, c.status);
+    RestResponse r = deleteChange(changeId, session);
+    assertEquals(204, r.getStatusCode());
+  }
+
+  @Test
+  public void publishDraftChange() throws GitAPIException,
+      IOException {
+    String changeId = createDraftChange();
+    ChangeInfo c = getChange(changeId);
+    assertEquals("p~master~" + changeId, c.id);
+    assertEquals(Change.Status.DRAFT, c.status);
+    RestResponse r = publishChange(changeId);
+    assertEquals(204, r.getStatusCode());
+    c = getChange(changeId);
+    assertEquals(Change.Status.NEW, c.status);
+  }
+
+  @Test
+  public void publishDraftPatchSet() throws GitAPIException,
+      IOException, OrmException {
+    String changeId = createDraftChange();
+    ChangeInfo c = getChange(changeId);
+    assertEquals("p~master~" + changeId, c.id);
+    assertEquals(Change.Status.DRAFT, c.status);
+    RestResponse r = publishPatchSet(changeId);
+    assertEquals(204, r.getStatusCode());
+    c = getChange(changeId);
+    assertEquals(Change.Status.NEW, c.status);
+  }
+
+  private ChangeInfo getChange(String changeId) throws IOException {
+    RestResponse r = session.get("/changes/?q=" + changeId);
+    List<ChangeInfo> c = (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<ChangeInfo>>() {}.getType());
+    return c.get(0);
+  }
+
+  private String createChange() throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, "refs/for/master").getChangeId();
+  }
+
+  private String createDraftChange() throws GitAPIException, IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, "refs/drafts/master").getChangeId();
+  }
+
+  private static RestResponse deleteChange(String changeId,
+      RestSession s) throws IOException {
+    return s.delete("/changes/" + changeId);
+  }
+
+  private RestResponse publishChange(String changeId) throws IOException {
+    return session.post("/changes/" + changeId + "/publish");
+  }
+
+  private RestResponse publishPatchSet(String changeId) throws IOException,
+    OrmException {
+    PatchSet patchSet = db.patchSets()
+        .get(Iterables.getOnlyElement(db.changes()
+            .byKey(new Change.Key(changeId)))
+            .currentPatchSetId());
+    return session.post("/changes/"
+        + changeId
+        + "/revisions/"
+        + patchSet.getRevision().get()
+        + "/publish");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
new file mode 100644
index 0000000..834b7c3
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.acceptance.git.PushOneCommit.Result;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class DeleteDraftPatchSetIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+  private TestAccount user;
+
+  private RestSession session;
+  private RestSession userSession;
+  private Git git;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    user = accounts.create("user", "user@example.com", "User");
+    session = new RestSession(server, admin);
+    userSession = new RestSession(server, user);
+    initSsh(admin);
+    Project.NameKey project = new Project.NameKey("p");
+    SshSession sshSession = new SshSession(server, admin);
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
+    sshSession.close();
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void deletePatchSet() throws GitAPIException,
+      IOException, OrmException {
+    String changeId = createChangeWith2PS("refs/for/master");
+    PatchSet ps = getCurrentPatchSet(changeId);
+    ChangeInfo c = getChange(changeId);
+    assertEquals("p~master~" + changeId, c.id);
+    assertEquals(Change.Status.NEW, c.status);
+    RestResponse r = deletePatchSet(changeId, ps, session);
+    assertEquals("Patch set is not a draft.", r.getEntityContent());
+    assertEquals(409, r.getStatusCode());
+  }
+
+  @Test
+  public void deleteDraftPatchSetNoACL() throws GitAPIException,
+      IOException, OrmException {
+    String changeId = createChangeWith2PS("refs/drafts/master");
+    PatchSet ps = getCurrentPatchSet(changeId);
+    ChangeInfo c = getChange(changeId);
+    assertEquals("p~master~" + changeId, c.id);
+    assertEquals(Change.Status.DRAFT, c.status);
+    RestResponse r = deletePatchSet(changeId, ps, userSession);
+    assertEquals("Not found", r.getEntityContent());
+    assertEquals(404, r.getStatusCode());
+  }
+
+  @Test
+  public void deleteDraftPatchSetAndChange() throws GitAPIException,
+      IOException, OrmException {
+    String changeId = createChangeWith2PS("refs/drafts/master");
+    PatchSet ps = getCurrentPatchSet(changeId);
+    ChangeInfo c = getChange(changeId);
+    assertEquals("p~master~" + changeId, c.id);
+    assertEquals(Change.Status.DRAFT, c.status);
+    RestResponse r = deletePatchSet(changeId, ps, session);
+    assertEquals(204, r.getStatusCode());
+    Change change = Iterables.getOnlyElement(db.changes().byKey(
+        new Change.Key(changeId)).toList());
+    assertEquals(1, db.patchSets().byChange(change.getId())
+        .toList().size());
+    ps = getCurrentPatchSet(changeId);
+    r = deletePatchSet(changeId, ps, session);
+    assertEquals(204, r.getStatusCode());
+    assertEquals(0, db.changes().byKey(new Change.Key(changeId))
+        .toList().size());
+  }
+
+  private String createChangeWith2PS(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    Result result = push.to(git, ref);
+    push = new PushOneCommit(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        "b.txt", "4711", result.getChangeId());
+    return push.to(git, ref).getChangeId();
+  }
+
+  private ChangeInfo getChange(String changeId) throws IOException {
+    RestResponse r = session.get("/changes/?q=" + changeId);
+    List<ChangeInfo> c = (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<ChangeInfo>>() {}.getType());
+    return c.get(0);
+  }
+
+  private PatchSet getCurrentPatchSet(String changeId) throws OrmException {
+    return db.patchSets()
+        .get(Iterables.getOnlyElement(db.changes()
+            .byKey(new Change.Key(changeId)))
+            .currentPatchSetId());
+  }
+
+  private static RestResponse deletePatchSet(String changeId,
+      PatchSet ps, RestSession s) throws IOException {
+    return s.delete("/changes/"
+        + changeId
+        + "/revisions/"
+        + ps.getRevision().get());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/GroupInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/GroupInfo.java
new file mode 100644
index 0000000..2c0efff
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/GroupInfo.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+public class GroupInfo {
+  public String id;
+  public String name;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ProjectConfigInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ProjectConfigInput.java
new file mode 100644
index 0000000..4d2e4b6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ProjectConfigInput.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+public class ProjectConfigInput {
+  public SubmitType submit_type;
+  public InheritableBoolean use_content_merge;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ReviewInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ReviewInput.java
new file mode 100644
index 0000000..a5371d2
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ReviewInput.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+public class ReviewInput {
+  Map<String, Integer> labels;
+
+  public static ReviewInput approve() {
+    ReviewInput in = new ReviewInput();
+    in.labels = Maps.newHashMap();
+    in.labels.put("Code-Review", 2);
+    return in;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
new file mode 100644
index 0000000..ccbbfb6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByCherryPickIT extends AbstractSubmit {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.CHERRY_PICK;
+  }
+
+  @Test
+  public void submitWithCherryPickIfFastForwardPossible() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    assertCherryPick(git, false);
+    assertEquals(change.getCommit().getParent(0),
+        getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithCherryPick() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submit(change2.getChangeId());
+    assertCherryPick(git, false);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+    submit(change.getChangeId());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+    submit(change2.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, change.getCommitId().getName());
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+    submit(change3.getChangeId());
+    assertCherryPick(git, true);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge_Conflict() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+
+  @Test
+  public void submitOutOfOrder() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    createChange(git, "Change 2", "b.txt", "other content");
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "c.txt", "different content");
+    submit(change3.getChangeId());
+    assertCherryPick(git, false);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitOutOfOrder_Conflict() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    createChange(git, "Change 2", "b.txt", "other content");
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "b.txt", "different content");
+    submitWithConflict(change3.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+}
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
new file mode 100644
index 0000000..9d56e18
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByFastForwardIT extends AbstractSubmit {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.FAST_FORWARD_ONLY;
+  }
+
+  @Test
+  public void submitWithFastForward() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(change.getCommitId(), head.getId());
+    assertEquals(oldHead, head.getParent(0));
+  }
+
+  @Test
+  public void submitFastForwardNotPossible_Conflict() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
new file mode 100644
index 0000000..6c671eb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByMergeAlwaysIT extends AbstractSubmitByMerge {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.MERGE_ALWAYS;
+  }
+
+  @Test
+  public void submitWithMergeIfFastForwardPossible() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(2, head.getParentCount());
+    assertEquals(oldHead, head.getParent(0));
+    assertEquals(change.getCommitId(), head.getParent(1));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
new file mode 100644
index 0000000..a5737a7
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -0,0 +1,35 @@
+package com.google.gerrit.acceptance.rest.change;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.MERGE_IF_NECESSARY;
+  }
+
+  @Test
+  public void submitWithFastForward() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(change.getCommitId(), head.getId());
+    assertEquals(oldHead, head.getParent(0));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
new file mode 100644
index 0000000..07594a6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.REBASE_IF_NECESSARY;
+  }
+
+  @Test
+  public void submitWithFastForward() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(change.getCommitId(), head.getId());
+    assertEquals(oldHead, head.getParent(0));
+  }
+
+  @Test
+  public void submitWithRebase() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submit(change2.getChangeId());
+    assertRebase(git, false);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+    submit(change.getChangeId());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+    submit(change2.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, change.getCommitId().getName());
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+    submit(change3.getChangeId());
+    assertRebase(git, true);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge_Conflict() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(oldHead, head);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitInput.java
new file mode 100644
index 0000000..8e1b340
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitInput.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+public class SubmitInput {
+  boolean wait_for_merge;
+
+  public static SubmitInput waitForMerge() {
+    SubmitInput in = new SubmitInput();
+    in.wait_for_merge = true;
+    return in;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewerInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewerInfo.java
new file mode 100644
index 0000000..212ee86
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewerInfo.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+public class SuggestReviewerInfo {
+  public AccountInfo account;
+  public GroupInfo group;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
new file mode 100644
index 0000000..1ed4d61
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SuggestReviewersIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+  private RestSession session;
+  private Git git;
+  private ReviewDb db;
+  private Project.NameKey project;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.admin();
+    session = new RestSession(server, admin);
+
+    group("users1");
+    group("users2");
+    group("users3");
+
+    accounts.create("user1", "user1@example.com", "User1", "users1");
+    accounts.create("user2", "user2@example.com", "User2", "users2");
+    accounts.create("user3", "user3@example.com", "User3", "users1", "users2");
+
+    initSsh(admin);
+    project = new Project.NameKey("p");
+    SshSession sshSession = new SshSession(server, admin);
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
+    sshSession.close();
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  @GerritConfig(name = "suggest.accounts", value = "false")
+  public void suggestReviewersNoResult1() throws GitAPIException, IOException,
+      Exception {
+    String changeId = createChange(admin);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 0);
+  }
+
+  @Test
+  @GerritConfigs(
+      {@GerritConfig(name = "suggest.accounts", value = "true"),
+       @GerritConfig(name = "suggest.from", value = "1"),
+       @GerritConfig(name = "accounts.visibility", value = "NONE"),
+      })
+  public void suggestReviewersNoResult2() throws GitAPIException, IOException,
+      Exception {
+    String changeId = createChange(admin);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 0);
+  }
+
+  @Test
+  @GerritConfig(name = "suggest.from", value = "2")
+  public void suggestReviewersNoResult3() throws GitAPIException, IOException,
+      Exception {
+    String changeId = createChange(admin);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 0);
+  }
+
+  @Test
+  public void suggestReviewersChange() throws GitAPIException,
+      IOException, Exception {
+    String changeId = createChange(admin);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 6);
+    reviewers = suggestReviewers(changeId, "u", 5);
+    assertEquals(reviewers.size(), 5);
+    reviewers = suggestReviewers(changeId, "users3", 10);
+    assertEquals(reviewers.size(), 1);
+  }
+
+  private List<SuggestReviewerInfo> suggestReviewers(String changeId,
+      String query, int n)
+      throws IOException {
+    return new Gson().fromJson(
+        session.get("/changes/"
+            + changeId
+            + "/suggest_reviewers?q="
+            + query
+            + "&n="
+            + n)
+        .getReader(),
+        new TypeToken<List<SuggestReviewerInfo>>() {}
+        .getType());
+  }
+
+  private void group(String name) throws IOException {
+    session.put("/groups/" + name, new Object()).consume();
+  }
+
+  private String createChange(TestAccount account) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, account.getIdent());
+    return push.to(git, "refs/for/master").getChangeId();
+  }
+}
\ No newline at end of file
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
index a254f15..08211d0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
@@ -32,7 +32,7 @@
 import com.google.gerrit.acceptance.rest.account.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupCache;
@@ -72,7 +72,7 @@
   @Before
   public void setUp() throws Exception {
     admin = accounts.create("admin", "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
     db = reviewDbProvider.open();
   }
 
@@ -223,9 +223,9 @@
       throws OrmException {
     AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
     Set<AccountGroup.UUID> ids = Sets.newHashSet();
-    ResultSet<AccountGroupIncludeByUuid> all =
-        db.accountGroupIncludesByUuid().byGroup(g.getId());
-    for (AccountGroupIncludeByUuid m : all) {
+    ResultSet<AccountGroupById> all =
+        db.accountGroupById().byGroup(g.getId());
+    for (AccountGroupById m : all) {
       ids.add(m.getIncludeUUID());
     }
     assertTrue(ids.size() == includes.length);
@@ -252,8 +252,8 @@
 
   private void assertNoIncludes(String group) throws OrmException {
     AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
-    Iterator<AccountGroupIncludeByUuid> it =
-        db.accountGroupIncludesByUuid().byGroup(g.getId()).iterator();
+    Iterator<AccountGroupById> it =
+        db.accountGroupById().byGroup(g.getId()).iterator();
     assertFalse(it.hasNext());
   }
 }
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
new file mode 100644
index 0000000..b6e017d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
@@ -0,0 +1,27 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
+  ],
+)
+
+java_library(
+  name = 'util',
+  srcs = [
+    'GroupAssert.java',
+    'GroupInfo.java',
+    'GroupInput.java',
+    'GroupOptionsInfo.java',
+    'GroupsInput.java',
+    'MembersInput.java',
+  ],
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//lib:gwtorm',
+    '//lib:junit',
+  ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
index 4641f09..b7ab0fc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
@@ -54,7 +54,7 @@
   public void setUp() throws Exception {
     admin = accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
@@ -88,7 +88,7 @@
   public void testCreateGroupWithoutCapability_Forbidden() throws OrmException,
       JSchException, IOException {
     TestAccount user = accounts.create("user", "user@example.com", "User");
-    RestResponse r = (new RestSession(user)).put("/groups/newGroup");
+    RestResponse r = (new RestSession(server, user)).put("/groups/newGroup");
     assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
index 5fe386d..67a7197 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
@@ -47,7 +47,7 @@
   public void setUp() throws Exception {
     admin = accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
index ab22367..05d74a3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
@@ -53,7 +53,7 @@
   public void setUp() throws Exception {
     admin = accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
index 997f476..f4c289c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.Nullable;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
@@ -36,8 +37,6 @@
 import java.util.Collection;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 public class ListGroupIncludesIT extends AbstractDaemonTest {
 
   @Inject
@@ -48,7 +47,7 @@
   @Before
   public void setUp() throws Exception {
     TestAccount admin = accounts.create("admin", "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
index a5aa3dd..0e7cc71 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.rest.account.AccountInfo;
+import com.google.gerrit.common.Nullable;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
@@ -37,8 +38,6 @@
 import java.util.Collection;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 public class ListGroupMembersIT extends AbstractDaemonTest {
 
   @Inject
@@ -49,7 +48,7 @@
   @Before
   public void setUp() throws Exception {
     TestAccount admin = accounts.create("admin", "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
index 07c7c93..4db2ac8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gson.Gson;
@@ -41,8 +42,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import javax.annotation.Nullable;
-
 public class ListGroupsIT extends AbstractDaemonTest {
 
   @Inject
@@ -58,7 +57,7 @@
   public void setUp() throws Exception {
     admin = accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
@@ -84,7 +83,7 @@
     expectedGroups.add("Anonymous Users");
     expectedGroups.add("Registered Users");
     TestAccount user = accounts.create("user", "user@example.com", "User");
-    RestResponse r = (new RestSession(user)).get("/groups/");
+    RestResponse r = new RestSession(server, user).get("/groups/");
     Map<String, GroupInfo> result =
         (new Gson()).fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
     assertGroups(expectedGroups, result.keySet());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/SystemGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/SystemGroupsIT.java
new file mode 100644
index 0000000..764b7e8
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/SystemGroupsIT.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An example test that tests presence of system groups in a newly initialized
+ * review site.
+ *
+ * The test shows how to perform these checks via SSH, REST or using Gerrit
+ * internals.
+ */
+public class SystemGroupsIT extends AbstractDaemonTest {
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  private AccountCreator accounts;
+
+  protected TestAccount admin;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@sap.com", "Administrator",
+            "Administrators");
+  }
+
+  @Test
+  public void systemGroupsCreated_ssh() throws JSchException, IOException {
+    SshSession session = new SshSession(server, admin);
+    String result = session.exec("gerrit ls-groups");
+    assertTrue(result.contains("Administrators"));
+    assertTrue(result.contains("Anonymous Users"));
+    assertTrue(result.contains("Non-Interactive Users"));
+    assertTrue(result.contains("Project Owners"));
+    assertTrue(result.contains("Registered Users"));
+    session.close();
+  }
+
+  @Test
+  public void systemGroupsCreated_rest() throws IOException {
+    RestSession session = new RestSession(server, admin);
+    RestResponse r = session.get("/groups/");
+    Gson gson = new Gson();
+    Map<String, GroupInfo> result =
+        gson.fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
+    Set<String> names = result.keySet();
+    assertTrue(names.contains("Administrators"));
+    assertTrue(names.contains("Anonymous Users"));
+    assertTrue(names.contains("Non-Interactive Users"));
+    assertTrue(names.contains("Project Owners"));
+    assertTrue(names.contains("Registered Users"));
+  }
+
+  @Test
+  public void systemGroupsCreated_internals() throws OrmException {
+    ReviewDb db = reviewDbProvider.open();
+    try {
+      Set<String> names = Sets.newHashSet();
+      for (AccountGroup g : db.accountGroups().all()) {
+        names.add(g.getName());
+      }
+      assertTrue(names.contains("Administrators"));
+      assertTrue(names.contains("Anonymous Users"));
+      assertTrue(names.contains("Non-Interactive Users"));
+      assertTrue(names.contains("Project Owners"));
+      assertTrue(names.contains("Registered Users"));
+    } finally {
+      db.close();
+    }
+  }
+}
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
new file mode 100644
index 0000000..bb3bb30
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -0,0 +1,38 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':branch',
+    ':project',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+  ],
+)
+
+java_library(
+  name = 'branch',
+  srcs = [
+    'BranchAssert.java',
+    'BranchInfo.java',
+  ],
+  deps = [
+    '//lib:guava',
+    '//lib:junit',
+  ],
+)
+
+java_library(
+  name = 'project',
+  srcs = [
+    'ProjectAssert.java',
+    'ProjectInfo.java',
+  ],
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:gwtorm',
+    '//lib:guava',
+    '//lib:junit',
+  ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
new file mode 100644
index 0000000..654ef65
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+public class BranchAssert {
+
+  public static void assertBranches(List<BranchInfo> expectedBranches,
+      List<BranchInfo> actualBranches) {
+    List<BranchInfo> missingBranches = Lists.newArrayList(actualBranches);
+    for (final BranchInfo b : expectedBranches) {
+      BranchInfo info =
+          Iterables.find(actualBranches, new Predicate<BranchInfo>() {
+            @Override
+            public boolean apply(BranchInfo info) {
+              return info.ref.equals(b.ref);
+            }
+          }, null);
+      assertNotNull("missing branch: " + b.ref, info);
+      assertBranchInfo(b, info);
+      missingBranches.remove(info);
+    }
+    assertTrue("unexpected branches: " + missingBranches,
+        missingBranches.isEmpty());
+  }
+
+  public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) {
+    assertEquals(expected.ref, actual.ref);
+    if (expected.revision != null) {
+      assertEquals(expected.revision, actual.revision);
+    }
+    assertEquals(expected.can_delete, toBoolean(actual.can_delete));
+  }
+
+  private static boolean toBoolean(Boolean b) {
+    if (b == null) {
+      return false;
+    }
+    return b.booleanValue();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchInfo.java
new file mode 100644
index 0000000..2b7933e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchInfo.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+public class BranchInfo {
+  public String ref;
+  public String revision;
+  public Boolean can_delete;
+
+  public BranchInfo() {
+  }
+
+  public BranchInfo(String ref, String revision, boolean canDelete) {
+    this.ref = ref;
+    this.revision = revision;
+    this.can_delete = canDelete;
+  }
+
+  @Override
+  public String toString() {
+    return ref;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 6afe8e6..44a40d3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -79,7 +79,7 @@
   public void setUp() throws Exception {
     admin = accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
@@ -210,7 +210,7 @@
   public void testCreateProjectWithoutCapability_Forbidden() throws OrmException,
       JSchException, IOException {
     TestAccount user = accounts.create("user", "user@example.com", "User");
-    RestResponse r = (new RestSession(user)).put("/projects/newProject");
+    RestResponse r = new RestSession(server, user).put("/projects/newProject");
     assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
new file mode 100644
index 0000000..d23ad6d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -0,0 +1,188 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class DeleteBranchIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
+  private AllProjectsNameProvider allProjects;
+
+  private RestSession adminSession;
+  private RestSession userSession;
+
+  private Project.NameKey project;
+  private Branch.NameKey branch;
+
+  @Before
+  public void setUp() throws Exception {
+    TestAccount admin = accounts.admin();
+    adminSession = new RestSession(server, admin);
+
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    userSession = new RestSession(server, user);
+
+    project = new Project.NameKey("p");
+    branch = new Branch.NameKey(project, "test");
+
+    initSsh(admin);
+    SshSession sshSession = new SshSession(server, admin);
+    try {
+      createProject(sshSession, project.get(), null, true);
+    } finally {
+      sshSession.close();
+    }
+
+    adminSession.put("/projects/" + project.get()
+        + "/branches/" + branch.getShortName()).consume();
+  }
+
+  @Test
+  public void deleteBranch_Forbidden() throws IOException {
+    RestResponse r =
+        userSession.delete("/projects/" + project.get()
+            + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void deleteBranchByAdmin() throws IOException {
+    RestResponse r =
+        adminSession.delete("/projects/" + project.get()
+            + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/projects/" + project.get()
+        + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void deleteBranchByProjectOwner() throws IOException,
+      ConfigInvalidException {
+    grantOwner();
+
+    RestResponse r =
+        userSession.delete("/projects/" + project.get()
+            + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    r.consume();
+
+    r = userSession.get("/projects/" + project.get()
+        + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void deleteBranchByAdminForcePushBlocked() throws IOException,
+      ConfigInvalidException {
+    blockForcePush();
+    RestResponse r =
+        adminSession.delete("/projects/" + project.get()
+            + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/projects/" + project.get()
+        + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden()
+      throws IOException, ConfigInvalidException {
+    grantOwner();
+    blockForcePush();
+    RestResponse r =
+        userSession.delete("/projects/" + project.get()
+            + "/branches/" + branch.getShortName());
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    r.consume();
+  }
+
+  private void blockForcePush() throws IOException, ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects.get());
+    md.setMessage(String.format("Block force %s", Permission.PUSH));
+    ProjectConfig config = ProjectConfig.read(md);
+    AccessSection s = config.getAccessSection("refs/heads/*", true);
+    Permission p = s.getPermission(Permission.PUSH, true);
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Anonymous Users"));
+    PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
+    rule.setForce(true);
+    rule.setBlock();
+    p.add(rule);
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  private void grantOwner() throws IOException, ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    md.setMessage(String.format("Grant %s", Permission.OWNER));
+    ProjectConfig config = ProjectConfig.read(md);
+    AccessSection s = config.getAccessSection("refs/*", true);
+    Permission p = s.getPermission(Permission.OWNER, true);
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Registered Users"));
+    PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
+    p.add(rule);
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index e55c52a..7057a4f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -24,9 +24,9 @@
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.GarbageCollectionQueue;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -47,9 +47,6 @@
   private AllProjectsName allProjects;
 
   @Inject
-  private GarbageCollectionQueue gcQueue;
-
-  @Inject
   private GcAssert gcAssert;
 
   private TestAccount admin;
@@ -63,7 +60,7 @@
         accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
 
-    SshSession sshSession = new SshSession(admin);
+    SshSession sshSession = new SshSession(server, admin);
 
     project1 = new Project.NameKey("p1");
     createProject(sshSession, project1.get());
@@ -71,7 +68,7 @@
     project2 = new Project.NameKey("p2");
     createProject(sshSession, project2.get());
 
-    session = new RestSession(admin);
+    session = new RestSession(server, admin);
   }
 
   @Test
@@ -82,13 +79,13 @@
   @Test
   public void testGcNotAllowed_Forbidden() throws IOException, OrmException, JSchException {
     assertEquals(HttpStatus.SC_FORBIDDEN,
-        new RestSession(accounts.create("user", "user@example.com", "User"))
+        new RestSession(server, accounts.create("user", "user@example.com", "User"))
             .post("/projects/" + allProjects.get() + "/gc").getStatusCode());
   }
 
   @Test
+  @UseLocalDisk
   public void testGcOneProject() throws JSchException, IOException {
-
     assertEquals(HttpStatus.SC_OK, POST("/projects/" + allProjects.get() + "/gc"));
     gcAssert.assertHasPackFile(allProjects);
     gcAssert.assertHasNoPackFile(project1, project2);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
new file mode 100644
index 0000000..bf8aac1b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GetChildProjectIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin =
+        accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(server, admin);
+  }
+
+  @Test
+  public void getNonExistingChildProject_NotFound() throws IOException {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        GET("/projects/" + allProjects.get() + "/children/non-existing").getStatusCode());
+  }
+
+  @Test
+  public void getNonChildProject_NotFound() throws IOException, JSchException {
+    SshSession sshSession = new SshSession(server, admin);
+    Project.NameKey p1 = new Project.NameKey("p1");
+    createProject(sshSession, p1.get());
+    Project.NameKey p2 = new Project.NameKey("p2");
+    createProject(sshSession, p2.get());
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        GET("/projects/" + p1.get() + "/children/" + p2.get()).getStatusCode());
+  }
+
+  @Test
+  public void getChildProject() throws IOException, JSchException {
+    SshSession sshSession = new SshSession(server, admin);
+    Project.NameKey child = new Project.NameKey("p1");
+    createProject(sshSession, child.get());
+    RestResponse r = GET("/projects/" + allProjects.get() + "/children/" + child.get());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    ProjectInfo childInfo =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<ProjectInfo>() {}.getType());
+    assertProjectInfo(projectCache.get(child).getProject(), childInfo);
+  }
+
+  @Test
+  public void getGrandChildProject_NotFound() throws IOException, JSchException {
+    SshSession sshSession = new SshSession(server, admin);
+    Project.NameKey child = new Project.NameKey("p1");
+    createProject(sshSession, child.get());
+    Project.NameKey grandChild = new Project.NameKey("p1.1");
+    createProject(sshSession, grandChild.get(), child);
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        GET("/projects/" + allProjects.get() + "/children/" + grandChild.get())
+            .getStatusCode());
+  }
+
+  @Test
+  public void getGrandChildProjectWithRecursiveFlag() throws IOException,
+      JSchException {
+    SshSession sshSession = new SshSession(server, admin);
+    Project.NameKey child = new Project.NameKey("p1");
+    createProject(sshSession, child.get());
+    Project.NameKey grandChild = new Project.NameKey("p1.1");
+    createProject(sshSession, grandChild.get(), child);
+    RestResponse r =
+        GET("/projects/" + allProjects.get() + "/children/" + grandChild.get()
+            + "?recursive");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    ProjectInfo grandChildInfo =
+        (new Gson()).fromJson(r.getReader(), new TypeToken<ProjectInfo>() {}.getType());
+    assertProjectInfo(projectCache.get(grandChild).getProject(), grandChildInfo);
+  }
+
+  private RestResponse GET(String endpoint) throws IOException {
+    return session.get(endpoint);
+  }
+}
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
new file mode 100644
index 0000000..4778662
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -0,0 +1,227 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+public class ListBranchesIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+  private RestSession session;
+  private SshSession sshSession;
+  private Project.NameKey project;
+  private Git git;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(server, admin);
+
+    project = new Project.NameKey("p");
+    initSsh(admin);
+    sshSession = new SshSession(server, admin);
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void listBranchesOfNonExistingProject_NotFound() throws IOException {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        GET("/projects/non-existing/branches").getStatusCode());
+  }
+
+  @Test
+  public void listBranchesOfNonVisibleProject_NotFound() throws IOException,
+      OrmException, JSchException, ConfigInvalidException {
+    blockRead(project, "refs/*");
+    RestSession session =
+        new RestSession(server, accounts.create("user", "user@example.com", "User"));
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        session.get("/projects/" + project.get() + "/branches").getStatusCode());
+  }
+
+  @Test
+  public void listBranchesOfEmptyProject() throws IOException, JSchException {
+    Project.NameKey emptyProject = new Project.NameKey("empty");
+    createProject(sshSession, emptyProject.get(), null, false);
+    RestResponse r = session.get("/projects/" + emptyProject.get() + "/branches");
+    List<BranchInfo> result =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<List<BranchInfo>>() {}.getType());
+    List<BranchInfo> expected = Lists.asList(
+        new BranchInfo("refs/meta/config",  null, false),
+        new BranchInfo[] {
+          new BranchInfo("HEAD", null, false)
+        });
+    assertBranches(expected, result);
+  }
+
+  @Test
+  public void listBranches() throws IOException, GitAPIException {
+    pushTo("refs/heads/master");
+    String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+    pushTo("refs/heads/dev");
+    String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+    RestResponse r = session.get("/projects/" + project.get() + "/branches");
+    List<BranchInfo> result =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<List<BranchInfo>>() {}.getType());
+    List<BranchInfo> expected = Lists.asList(
+        new BranchInfo("refs/meta/config",  null, false),
+        new BranchInfo[] {
+          new BranchInfo("HEAD", "master", false),
+          new BranchInfo("refs/heads/master", masterCommit, false),
+          new BranchInfo("refs/heads/dev", devCommit, true)
+        });
+    assertBranches(expected, result);
+
+    // verify correct sorting
+    assertEquals("HEAD", result.get(0).ref);
+    assertEquals("refs/meta/config", result.get(1).ref);
+    assertEquals("refs/heads/dev", result.get(2).ref);
+    assertEquals("refs/heads/master", result.get(3).ref);
+  }
+
+  @Test
+  public void listBranchesSomeHidden() throws IOException, GitAPIException,
+      ConfigInvalidException, OrmException, JSchException {
+    blockRead(project, "refs/heads/dev");
+    RestSession session =
+        new RestSession(server, accounts.create("user", "user@example.com", "User"));
+    pushTo("refs/heads/master");
+    String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+    pushTo("refs/heads/dev");
+    RestResponse r = session.get("/projects/" + project.get() + "/branches");
+    List<BranchInfo> result =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<List<BranchInfo>>() {}.getType());
+    // refs/meta/config is hidden since user is no project owner
+    List<BranchInfo> expected = Lists.asList(
+        new BranchInfo("HEAD", "master", false),
+        new BranchInfo[] {
+          new BranchInfo("refs/heads/master", masterCommit, false),
+        });
+    assertBranches(expected, result);
+  }
+
+  @Test
+  public void listBranchesHeadHidden() throws IOException, GitAPIException,
+      ConfigInvalidException, OrmException, JSchException {
+    blockRead(project, "refs/heads/master");
+    RestSession session =
+        new RestSession(server, accounts.create("user", "user@example.com", "User"));
+    pushTo("refs/heads/master");
+    pushTo("refs/heads/dev");
+    String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+    RestResponse r = session.get("/projects/" + project.get() + "/branches");
+    List<BranchInfo> result =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<List<BranchInfo>>() {}.getType());
+    // refs/meta/config is hidden since user is no project owner
+    assertBranches(Collections.singletonList(new BranchInfo("refs/heads/dev",
+        devCommit, false)), result);
+  }
+
+  private RestResponse GET(String endpoint) throws IOException {
+    return session.get(endpoint);
+  }
+
+  private void blockRead(Project.NameKey project, String ref)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    md.setMessage("Grant submit on " + ref);
+    ProjectConfig config = ProjectConfig.read(md);
+    AccessSection s = config.getAccessSection(ref, true);
+    Permission p = s.getPermission(Permission.READ, true);
+    AccountGroup adminGroup = groupCache.get(AccountGroup.REGISTERED_USERS);
+    PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
+    rule.setBlock();
+    p.add(rule);
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, ref);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
new file mode 100644
index 0000000..c03ecd3
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjects;
+
+import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+public class ListChildProjectsIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin =
+        accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(server, admin);
+  }
+
+  @Test
+  public void listChildrenOfNonExistingProject_NotFound() throws IOException {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        GET("/projects/non-existing/children/").getStatusCode());
+  }
+
+  @Test
+  public void listNoChildren() throws IOException {
+    RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    List<ProjectInfo> children =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<List<ProjectInfo>>() {}.getType());
+    assertTrue(children.isEmpty());
+  }
+
+  @Test
+  public void listChildren() throws IOException, JSchException {
+    SshSession sshSession = new SshSession(server, admin);
+    Project.NameKey child1 = new Project.NameKey("p1");
+    createProject(sshSession, child1.get());
+    Project.NameKey child2 = new Project.NameKey("p2");
+    createProject(sshSession, child2.get());
+    createProject(sshSession, "p1.1", child1);
+
+    RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    List<ProjectInfo> children =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<List<ProjectInfo>>() {}.getType());
+    assertProjects(Arrays.asList(child1, child2), children);
+  }
+
+  @Test
+  public void listChildrenRecursively() throws IOException, JSchException {
+    SshSession sshSession = new SshSession(server, admin);
+    Project.NameKey child1 = new Project.NameKey("p1");
+    createProject(sshSession, child1.get());
+    createProject(sshSession, "p2");
+    Project.NameKey child1_1 = new Project.NameKey("p1.1");
+    createProject(sshSession, child1_1.get(), child1);
+    Project.NameKey child1_2 = new Project.NameKey("p1.2");
+    createProject(sshSession, child1_2.get(), child1);
+    Project.NameKey child1_1_1 = new Project.NameKey("p1.1.1");
+    createProject(sshSession, child1_1_1.get(), child1_1);
+    Project.NameKey child1_1_1_1 = new Project.NameKey("p1.1.1.1");
+    createProject(sshSession, child1_1_1_1.get(), child1_1_1);
+
+    RestResponse r = GET("/projects/" + child1.get() + "/children/?recursive");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    List<ProjectInfo> children =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<List<ProjectInfo>>() {}.getType());
+    assertProjects(Arrays.asList(child1_1, child1_2, child1_1_1, child1_1_1_1), children);
+  }
+
+  private RestResponse GET(String endpoint) throws IOException {
+    return session.get(endpoint);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 25ccbee..224d59d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -15,18 +15,36 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectState;
 
+import java.util.List;
 import java.util.Set;
 
 public class ProjectAssert {
 
+  public static void assertProjects(Iterable<Project.NameKey> expected,
+      List<ProjectInfo> actual) {
+    for (final Project.NameKey p : expected) {
+      ProjectInfo info = Iterables.find(actual, new Predicate<ProjectInfo>() {
+        @Override
+        public boolean apply(ProjectInfo info) {
+          return new Project.NameKey(info.name).equals(p);
+        }}, null);
+      assertNotNull("missing project: " + p, info);
+      actual.remove(info);
+    }
+    assertTrue("unexpected projects: " + actual, actual.isEmpty());
+  }
+
   public static void assertProjectInfo(Project project, ProjectInfo info) {
     if (info.name != null) {
       // 'name' is not set if returned in a map
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
index 72dd2d6..2e209d1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
@@ -19,4 +19,9 @@
   public String name;
   public String parent;
   public String description;
+
+  @Override
+  public String toString() {
+    return name;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
new file mode 100644
index 0000000..abc3bb6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SetParentIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private AllProjectsNameProvider allProjects;
+
+  private RestSession adminSession;
+  private RestSession userSession;
+  private SshSession sshSession;
+
+  private String project;
+
+  @Before
+  public void setUp() throws Exception {
+    TestAccount admin = accounts.admin();
+    adminSession = new RestSession(server, admin);
+
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    userSession = new RestSession(server, user);
+
+
+    initSsh(admin);
+    sshSession = new SshSession(server, admin);
+    project = "p";
+    createProject(sshSession, project, null, true);
+  }
+
+  @After
+  public void cleanup() {
+    sshSession.close();
+  }
+
+  @Test
+  public void setParent_Forbidden() throws IOException, JSchException {
+    String parent = "parent";
+    createProject(sshSession, parent, null, true);
+    RestResponse r =
+        userSession.put("/projects/" + project + "/parent",
+            new ParentInput(parent));
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void setParent() throws IOException, JSchException {
+    String parent = "parent";
+    createProject(sshSession, parent, null, true);
+    RestResponse r =
+        adminSession.put("/projects/" + project + "/parent",
+            new ParentInput(parent));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/projects/" + project + "/parent");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    String newParent =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<String>() {}.getType());
+    assertEquals(parent, newParent);
+    r.consume();
+  }
+
+  @Test
+  public void setParentForAllProjects_Conflict() throws IOException {
+    RestResponse r =
+        adminSession.put("/projects/" + allProjects.get() + "/parent",
+            new ParentInput(project));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void setInvalidParent_Conflict() throws IOException, JSchException {
+    RestResponse r =
+        adminSession.put("/projects/" + project + "/parent",
+            new ParentInput(project));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+
+    String child = "child";
+    createProject(sshSession, child, new Project.NameKey(project), true);
+    r = adminSession.put("/projects/" + project + "/parent",
+           new ParentInput(child));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+
+    String grandchild = "grandchild";
+    createProject(sshSession, grandchild, new Project.NameKey(child), true);
+    r = adminSession.put("/projects/" + project + "/parent",
+           new ParentInput(grandchild));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void setNonExistingParent_UnprocessibleEntity() throws IOException {
+    RestResponse r =
+        adminSession.put("/projects/" + project + "/parent",
+            new ParentInput("non-existing"));
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    r.consume();
+  }
+
+  @SuppressWarnings("unused")
+  private static class ParentInput {
+    String parent;
+
+    ParentInput(String parent) {
+      this.parent = parent;
+    }
+  }
+}
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
new file mode 100644
index 0000000..94e6f6a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
@@ -0,0 +1,6 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = glob(['*IT.java']),
+  deps = ['//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 9c1e7d0..f10258c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.GcAssert;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -70,7 +71,7 @@
         accounts.create("admin", "admin@example.com", "Administrator",
             "Administrators");
 
-    sshSession = new SshSession(admin);
+    sshSession = new SshSession(server, admin);
 
     project1 = new Project.NameKey("p1");
     createProject(sshSession, project1.get());
@@ -83,6 +84,7 @@
   }
 
   @Test
+  @UseLocalDisk
   public void testGc() throws JSchException, IOException {
     String response =
         sshSession.exec("gerrit gc \"" + project1.get() + "\" \""
@@ -94,6 +96,7 @@
   }
 
   @Test
+  @UseLocalDisk
   public void testGcAll() throws JSchException, IOException {
     String response = sshSession.exec("gerrit gc --all");
     assertFalse(sshSession.hasError());
@@ -104,12 +107,13 @@
   @Test
   public void testGcWithoutCapability_Error() throws IOException, OrmException,
       JSchException {
-    SshSession s = new SshSession(accounts.create("user", "user@example.com", "User"));
+    SshSession s = new SshSession(server, accounts.create("user", "user@example.com", "User"));
     s.exec("gerrit gc --all");
-    assertError("fatal: user does not have \"runGC\" capability.", s.getError());
+    assertError("Capability runGC is required to access this resource", s.getError());
   }
 
   @Test
+  @UseLocalDisk
   public void testGcAlreadyScheduled() {
     gcQueue.addAll(Arrays.asList(project1));
     GarbageCollectionResult result = garbageCollectionFactory.create().run(
diff --git a/gerrit-acceptance-tests/tests.defs b/gerrit-acceptance-tests/tests.defs
new file mode 100644
index 0000000..f125a39
--- /dev/null
+++ b/gerrit-acceptance-tests/tests.defs
@@ -0,0 +1,20 @@
+def acceptance_tests(
+    srcs,
+    deps = [],
+    vm_args = ['-Xmx128m']):
+  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',
+      ],
+      labels = [
+        'acceptance',
+        'slow',
+      ],
+      vm_args = vm_args,
+    )
diff --git a/gerrit-antlr/BUCK b/gerrit-antlr/BUCK
new file mode 100644
index 0000000..57e7e1d
--- /dev/null
+++ b/gerrit-antlr/BUCK
@@ -0,0 +1,36 @@
+PARSER_DEPS = [
+  ':query_exception',
+  '//lib/antlr:java_runtime',
+]
+
+java_library(
+  name = 'query_exception',
+  srcs = ['src/main/java/com/google/gerrit/server/query/QueryParseException.java'],
+  visibility = ['PUBLIC'],
+)
+
+genantlr(
+  name = 'query_antlr',
+  srcs = ['src/main/antlr3/com/google/gerrit/server/query/Query.g'],
+  out = 'query_antlr.src.zip',
+)
+
+java_library(
+  name = 'lib',
+  srcs = [genfile('query_antlr.src.zip')],
+  deps = PARSER_DEPS + [':query_antlr'],
+)
+
+# Hack necessary to expose ANTLR generated code as JAR to Eclipse.
+genrule(
+  name = 'query_link',
+  cmd = 'ln -s $(location :lib) $OUT',
+  deps = [':lib'],
+  out = 'query_parser.jar',
+)
+prebuilt_jar(
+  name = 'query_parser',
+  binary_jar = genfile('query_parser.jar'),
+  deps = PARSER_DEPS + [':query_link'],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
deleted file mode 100644
index c17e01e..0000000
--- a/gerrit-antlr/pom.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-antlr</artifactId>
-  <name>Gerrit Code Review - ANTLR</name>
-
-  <description>
-    ANTLR generated sources
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>org.antlr</groupId>
-      <artifactId>antlr</artifactId>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.antlr</groupId>
-        <artifactId>antlr3-maven-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>antlr</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java b/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java
index 97eb2bf..1f69ba7 100644
--- a/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java
+++ b/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.server.query;
 
+/**
+ * Exception thrown when a search query is invalid.
+ * <p>
+ * <b>NOTE:</b> the message is visible to end users.
+ */
 public class QueryParseException extends Exception {
   private static final long serialVersionUID = 1L;
 
diff --git a/gerrit-cache-h2/BUCK b/gerrit-cache-h2/BUCK
new file mode 100644
index 0000000..d3e8994
--- /dev/null
+++ b/gerrit-cache-h2/BUCK
@@ -0,0 +1,14 @@
+java_library(
+  name = 'cache-h2',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-server:server',
+    '//lib:guava',
+    '//lib:h2',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-cache-h2/pom.xml b/gerrit-cache-h2/pom.xml
deleted file mode 100644
index 80ab03b..0000000
--- a/gerrit-cache-h2/pom.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-cache-h2</artifactId>
-  <name>Gerrit Code Review - Guava + H2 caching</name>
-
-  <description>
-    Implementation of caching backed by Guava and H2
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-server</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.h2database</groupId>
-      <artifactId>h2</artifactId>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
index 5a600c0..1c850d1 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
@@ -85,7 +85,7 @@
         "cache", def.name(), "memoryLimit",
         def.maximumWeight()));
 
-    builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
+    builder = builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
 
     Weigher<K, V> weigher = def.weigher();
     if (weigher != null && unwrapValueHolder) {
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 4428034..4aca42b 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -11,6 +11,7 @@
 import com.google.common.hash.Funnel;
 import com.google.common.hash.Funnels;
 import com.google.common.hash.PrimitiveSink;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.TypeLiteral;
 
 import org.h2.jdbc.JdbcSQLException;
@@ -115,7 +116,7 @@
   @Override
   public void put(final K key, V val) {
     final ValueHolder<V> h = new ValueHolder<V>(val);
-    h.created = System.currentTimeMillis();
+    h.created = TimeUtil.nowMs();
     mem.put(key, h);
     executor.execute(new Runnable() {
       @Override
@@ -183,7 +184,7 @@
     cal.set(Calendar.MILLISECOND, 0);
     cal.add(Calendar.DAY_OF_MONTH, 1);
 
-    long delay = cal.getTimeInMillis() - System.currentTimeMillis();
+    long delay = cal.getTimeInMillis() - TimeUtil.nowMs();
     service.schedule(new Runnable() {
       @Override
       public void run() {
@@ -246,7 +247,7 @@
       }
 
       final ValueHolder<V> h = new ValueHolder<V>(loader.load(key));
-      h.created = System.currentTimeMillis();
+      h.created = TimeUtil.nowMs();
       executor.execute(new Runnable() {
         @Override
         public void run() {
@@ -322,7 +323,7 @@
       @SuppressWarnings("unchecked")
       @Override
       Funnel<String> funnel() {
-        Funnel<?> s = Funnels.stringFunnel();
+        Funnel<?> s = Funnels.unencodedCharsFunnel();
         return (Funnel<String>) s;
       }
     };
@@ -462,7 +463,7 @@
         c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
       }
       try {
-        c.touch.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
+        c.touch.setTimestamp(1, TimeUtil.nowTs());
         keyType.set(c.touch, 2, key);
         c.touch.executeUpdate();
       } finally {
@@ -491,7 +492,7 @@
           keyType.set(c.put, 1, key);
           c.put.setObject(2, holder.value, Types.JAVA_OBJECT);
           c.put.setTimestamp(3, new Timestamp(holder.created));
-          c.put.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
+          c.put.setTimestamp(4, TimeUtil.nowTs());
           c.put.executeUpdate();
           holder.clean = true;
         } finally {
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
new file mode 100644
index 0000000..94b0a01
--- /dev/null
+++ b/gerrit-common/BUCK
@@ -0,0 +1,40 @@
+SRC = 'src/main/java/com/google/gerrit/'
+
+gwt_module(
+  name = 'client',
+  srcs = glob([SRC + 'common/**/*.java']),
+  gwtxml = SRC + 'Common.gwt.xml',
+  deps = [
+    '//gerrit-patch-jgit:client',
+    '//gerrit-prettify:client',
+    '//gerrit-reviewdb:client',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'server',
+  srcs = glob([SRC + 'common/**/*.java']),
+  deps = [
+    '//gerrit-patch-jgit:server',
+    '//gerrit-prettify:server',
+    '//gerrit-reviewdb:server',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_test(
+  name = 'client_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':client',
+    '//lib:junit',
+  ],
+  source_under_test = [':client'],
+)
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
deleted file mode 100644
index efa7285..0000000
--- a/gerrit-common/pom.xml
+++ /dev/null
@@ -1,112 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-common</artifactId>
-  <name>Gerrit Code Review - Common</name>
-
-  <description>
-    Classes common to both server and client.
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-servlet</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-gwtexpui</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-reviewdb</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-prettify</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-patch-jgit</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.code.findbugs</groupId>
-      <artifactId>jsr305</artifactId>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-antrun-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>generate-version</id>
-            <phase>generate-resources</phase>
-            <configuration>
-              <target>
-                <property name="dst" location="${project.build.outputDirectory}" />
-                <property name="pkg" location="${dst}/com/google/gerrit/common" />
-                <mkdir dir="${pkg}" />
-                <exec executable="git" outputproperty="v">
-                  <arg value="describe"/>
-                  <arg value="HEAD"/>
-                </exec>
-                <echo file="${pkg}/Version">${v}</echo>
-              </target>
-            </configuration>
-            <goals>
-              <goal>run</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java b/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
deleted file mode 100644
index 42ee5dd..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common;
-
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.TextResource;
-
-public interface ClientVersion extends ClientBundle {
-  /** Version number of this client software build. */
-  @Source("Version")
-  TextResource version();
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/Nullable.java b/gerrit-common/src/main/java/com/google/gerrit/common/Nullable.java
new file mode 100644
index 0000000..46db282
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/Nullable.java
@@ -0,0 +1,26 @@
+// 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.common;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Gerrit's own replacement for the javax.annotations.Nullable
+ */
+@Retention(RUNTIME)
+public @interface Nullable {
+}
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 beab1d9..9e64aae 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
@@ -54,6 +54,10 @@
     return "/c/" + c + "/";
   }
 
+  public static String toChange(Change.Id c, String p) {
+    return "/c/" + c + "/" + p;
+  }
+
   public static String toChange(final PatchSet.Id ps) {
     return "/c/" + ps.getParentKey() + "/" + ps.get();
   }
@@ -90,6 +94,10 @@
     return PROJECTS + name.get() + DASHBOARDS + id;
   }
 
+  public static String toProjectDefaultDashboard(Project.NameKey name) {
+    return PROJECTS + name.get() + DASHBOARDS + "default";
+  }
+
   public static String projectQuery(Project.NameKey proj) {
     return op("project", proj.get());
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
index f5ef9c5..276c332 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
@@ -37,7 +37,19 @@
   DETAILED_ACCOUNTS(7),
 
   /** Include messages associated with the change. */
-  MESSAGES(9);
+  MESSAGES(9),
+
+  /** Include allowed actions client could perform. */
+  CURRENT_ACTIONS(10),
+
+  /** Set the reviewed boolean for the caller. */
+  REVIEWED(11),
+
+  /** Include draft comments for the caller. */
+  DRAFT_COMMENTS(12),
+
+  /** Include download commands for the caller. */
+  DOWNLOAD_COMMANDS(13);
 
   private final int value;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java
new file mode 100644
index 0000000..777467d
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java
@@ -0,0 +1,20 @@
+// 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.common.changes;
+
+/** The side on which a comment was added. */
+public enum Side {
+  PARENT, REVISION;
+}
\ No newline at end of file
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index c2d03ab..f382d49 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -31,32 +30,10 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface AccountSecurity extends RemoteJsonService {
-  @SignInRequired
-  void mySshKeys(AsyncCallback<List<AccountSshKey>> callback);
-
-  @Audit
-  @SignInRequired
-  void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback);
-
-  @Audit
-  @SignInRequired
-  void deleteSshKeys(Set<AccountSshKey.Id> ids,
-      AsyncCallback<VoidResult> callback);
-
   @Audit
   @SignInRequired
   void changeUserName(String newName, AsyncCallback<VoidResult> callback);
 
-  @Audit
-  @SignInRequired
-  void generatePassword(AccountExternalId.Key key,
-      AsyncCallback<AccountExternalId> callback);
-
-  @Audit
-  @SignInRequired
-  void clearPassword(AccountExternalId.Key key,
-      AsyncCallback<AccountExternalId> gerritCallback);
-
   @SignInRequired
   void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
 
@@ -77,9 +54,5 @@
 
   @Audit
   @SignInRequired
-  void registerEmail(String address, AsyncCallback<Account> callback);
-
-  @Audit
-  @SignInRequired
   void validateEmail(String token, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java
deleted file mode 100644
index 6facf73..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java
+++ /dev/null
@@ -1,109 +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.common.data;
-
-public class AddBranchResult {
-  protected ListBranchesResult listBranchesResult;
-  protected Error error;
-
-  protected AddBranchResult() {
-  }
-
-  public AddBranchResult(final Error error) {
-    this.error = error;
-  }
-
-  public AddBranchResult(final ListBranchesResult listBranchesResult) {
-    this.listBranchesResult = listBranchesResult;
-  }
-
-  public ListBranchesResult getListBranchesResult() {
-    return listBranchesResult;
-  }
-
-  public boolean hasError() {
-    return error != null;
-  }
-
-  public Error getError() {
-    return error;
-  }
-
-  @Override
-  public String toString() {
-    if (hasError()) {
-      return getError().toString();
-    }
-    if (getListBranchesResult() != null) {
-      return "succeed, no repository: "
-          + getListBranchesResult().getNoRepository() + ", can add: "
-          + getListBranchesResult().getCanAdd();
-    }
-    return "succeed";
-  }
-
-  public static class Error {
-    public static enum Type {
-      /** The branch cannot be created because the given branch name is invalid. */
-      INVALID_NAME,
-
-      /** The branch cannot be created because the given revision is invalid. */
-      INVALID_REVISION,
-
-      /**
-       * The branch cannot be created under the given refname prefix (e.g
-       * branches cannot be created under magic refname prefixes).
-       */
-      BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX,
-
-      /** The branch that should be created exists already. */
-      BRANCH_ALREADY_EXISTS,
-
-      /**
-       * The branch cannot be created because it conflicts with an existing
-       * branch (branches cannot be nested).
-       */
-      BRANCH_CREATION_CONFLICT
-    }
-
-    protected Type type;
-    protected String refname;
-
-    protected Error() {
-    }
-
-    public Error(final Type type) {
-      this(type, null);
-    }
-
-    public Error(final Type type, final String refname) {
-      this.type = type;
-      this.refname = refname;
-    }
-
-    public Type getType() {
-      return type;
-    }
-
-    public String getRefname() {
-      return refname;
-    }
-
-    @Override
-    public String toString() {
-      return type + " " + refname;
-    }
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index f6d5ea33..3342bc2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -28,6 +28,7 @@
   protected boolean allowsAnonymous;
   protected boolean canAbandon;
   protected boolean canEditCommitMessage;
+  protected boolean canCherryPick;
   protected boolean canPublish;
   protected boolean canRebase;
   protected boolean canRestore;
@@ -84,6 +85,14 @@
     canEditCommitMessage = a;
   }
 
+  public boolean canCherryPick() {
+    return canCherryPick;
+  }
+
+  public void setCanCherryPick(final boolean a) {
+    canCherryPick = a;
+  }
+
   public boolean canPublish() {
     return canPublish;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
deleted file mode 100644
index 0c466497..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-public interface ChangeListService extends RemoteJsonService {
-  /**
-   * Add and/or remove changes from the set of starred changes of the caller.
-   *
-   * @param req the add and remove cluster.
-   */
-  @Audit
-  @SignInRequired
-  void toggleStars(ToggleStarRequest req, AsyncCallback<VoidResult> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index 7660a80..c4c388f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -26,8 +27,12 @@
 public class GerritConfig implements Cloneable {
   protected String registerUrl;
   protected String registerText;
+  protected String loginUrl;
+  protected String loginText;
+  protected String switchAccountUrl;
   protected String httpPasswordUrl;
   protected String reportBugUrl;
+  protected boolean gitBasicAuth;
 
   protected GitwebConfig gitweb;
   protected boolean useContributorAgreements;
@@ -46,6 +51,24 @@
   protected boolean testChangeMerge;
   protected String anonymousCowardName;
   protected int suggestFrom;
+  protected int changeUpdateDelay;
+  protected AccountGeneralPreferences.ChangeScreen changeScreen;
+
+  public String getLoginUrl() {
+    return loginUrl;
+  }
+
+  public void setLoginUrl(final String u) {
+    loginUrl = u;
+  }
+
+  public String getLoginText() {
+    return loginText;
+  }
+
+  public void setLoginText(String signinText) {
+    this.loginText = signinText;
+  }
 
   public String getRegisterUrl() {
     return registerUrl;
@@ -55,6 +78,14 @@
     registerUrl = u;
   }
 
+  public String getSwitchAccountUrl() {
+    return switchAccountUrl;
+  }
+
+  public void setSwitchAccountUrl(String u) {
+    switchAccountUrl = u;
+  }
+
   public String getRegisterText() {
     return registerText;
   }
@@ -71,6 +102,14 @@
     reportBugUrl = u;
   }
 
+  public boolean isGitBasicAuth() {
+    return gitBasicAuth;
+  }
+
+  public void setGitBasicAuth(boolean gba) {
+    gitBasicAuth = gba;
+  }
+
   public String getEditFullNameUrl() {
     return editFullNameUrl;
   }
@@ -225,4 +264,20 @@
     }
     return true;
   }
+
+  public int getChangeUpdateDelay() {
+    return changeUpdateDelay;
+  }
+
+  public void setChangeUpdateDelay(int seconds) {
+    changeUpdateDelay = seconds;
+  }
+
+  public AccountGeneralPreferences.ChangeScreen getChangeScreen() {
+    return changeScreen;
+  }
+
+  public void setChangeScreen(AccountGeneralPreferences.ChangeScreen ui) {
+    this.changeScreen = ui;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
index 8528c0f..5ef6d6a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
@@ -78,6 +78,9 @@
   /** Whether to include links to draft patch sets */
   private boolean linkDrafts;
 
+  /** Whether to encode URL segments */
+  private boolean urlEncode;
+
   /** Private default constructor for gson. */
   protected GitWebType() {
   }
@@ -150,7 +153,7 @@
   /**
    * Set the pattern for link-name type.
    *
-   * @param pattern The pattern for link-name type
+   * @param name The link-name type
    */
   public void setLinkName(final String name) {
     if (name != null && !name.isEmpty()) {
@@ -217,4 +220,12 @@
   public void setLinkDrafts(boolean linkDrafts) {
     this.linkDrafts = linkDrafts;
   }
+
+  public boolean isUrlEncode() {
+    return urlEncode;
+  }
+
+  public void setUrlEncode(boolean urlEncode) {
+    this.urlEncode = urlEncode;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index 8c08feb..1fd98b6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -58,6 +58,9 @@
   /** Can flush any cache except the active web_sessions cache. */
   public static final String FLUSH_CACHES = "flushCaches";
 
+  /** Can generate HTTP passwords for user other than self. */
+  public static final String GENERATE_HTTP_PASSWORD = "generateHttpPassword";
+
   /** Can terminate any task using the kill command. */
   public static final String KILL_TASK = "killTask";
 
@@ -67,12 +70,12 @@
   /** Maximum result limit per executed query. */
   public static final String QUERY_LIMIT = "queryLimit";
 
+  /** Ability to impersonate another user. */
+  public static final String RUN_AS = "runAs";
+
   /** Can run the Git garbage collection. */
   public static final String RUN_GC = "runGC";
 
-  /** Forcefully restart replication to any configured destination. */
-  public static final String START_REPLICATION = "startReplication";
-
   /** Can perform streaming of Gerrit events. */
   public static final String STREAM_EVENTS = "streamEvents";
 
@@ -100,8 +103,8 @@
     NAMES_ALL.add(KILL_TASK);
     NAMES_ALL.add(PRIORITY);
     NAMES_ALL.add(QUERY_LIMIT);
+    NAMES_ALL.add(RUN_AS);
     NAMES_ALL.add(RUN_GC);
-    NAMES_ALL.add(START_REPLICATION);
     NAMES_ALL.add(STREAM_EVENTS);
     NAMES_ALL.add(VIEW_CACHES);
     NAMES_ALL.add(VIEW_CONNECTIONS);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
index ccd50fc..86b0b39 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
-import javax.annotation.Nullable;
-
 /**
  * Group methods exposed by the GroupBackend.
  */
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
index f986600..63b4a04 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
-import javax.annotation.Nullable;
-
 /**
  * Utility class for building GroupDescription objects.
  */
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 0d2be51..c890812 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 
 import java.util.List;
@@ -24,7 +24,7 @@
   public AccountInfoCache accounts;
   public AccountGroup group;
   public List<AccountGroupMember> members;
-  public List<AccountGroupIncludeByUuid> includes;
+  public List<AccountGroupById> includes;
   public GroupReference ownerGroup;
   public boolean canModify;
 
@@ -43,7 +43,7 @@
     members = m;
   }
 
-  public void setIncludes(List<AccountGroupIncludeByUuid> i) {
+  public void setIncludes(List<AccountGroupById> i) {
     includes = i;
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
index 085973c..5a0561d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
@@ -55,7 +55,7 @@
    * <li>an anonymous info block, if <code>id</code> was not loaded.</li>
    * </ul>
    *
-   * @param id the id desired.
+   * @param uuid the id desired.
    * @return info block for the group.
    */
   public GroupInfo get(final AccountGroup.UUID uuid) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index f014f5f..f143405 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -21,6 +21,7 @@
 
 /** Data sent as part of the host page, to bootstrap the UI. */
 public class HostPageData {
+  public String version;
   public Account account;
   public AccountDiffPreference accountDiffPref;
   public String xGerritAuth;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index db8bde9..7fd8864 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -97,12 +97,15 @@
   protected String functionName;
   protected boolean copyMinScore;
   protected boolean copyMaxScore;
+  protected boolean copyAllScoresOnTrivialRebase;
+  protected boolean copyAllScoresIfNoCodeChange;
 
   protected List<LabelValue> values;
   protected short maxNegative;
   protected short maxPositive;
 
   private transient boolean canOverride;
+  private transient List<String> refPatterns;
   private transient List<Integer> intList;
   private transient Map<Short, LabelValue> byValue;
 
@@ -157,10 +160,18 @@
     return canOverride;
   }
 
+  public List<String> getRefPatterns() {
+    return refPatterns;
+  }
+
   public void setCanOverride(boolean canOverride) {
     this.canOverride = canOverride;
   }
 
+  public void setRefPatterns(List<String> refPatterns) {
+    this.refPatterns = refPatterns;
+  }
+
   public List<LabelValue> getValues() {
     return values;
   }
@@ -196,6 +207,22 @@
     this.copyMaxScore = copyMaxScore;
   }
 
+  public boolean isCopyAllScoresOnTrivialRebase() {
+    return copyAllScoresOnTrivialRebase;
+  }
+
+  public void setCopyAllScoresOnTrivialRebase(boolean copyAllScoresOnTrivialRebase) {
+    this.copyAllScoresOnTrivialRebase = copyAllScoresOnTrivialRebase;
+  }
+
+  public boolean isCopyAllScoresIfNoCodeChange() {
+    return copyAllScoresIfNoCodeChange;
+  }
+
+  public void setCopyAllScoresIfNoCodeChange(boolean copyAllScoresIfNoCodeChange) {
+    this.copyAllScoresIfNoCodeChange = copyAllScoresIfNoCodeChange;
+  }
+
   public boolean isMaxNegative(PatchSetApproval ca) {
     return maxNegative == ca.getValue();
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
deleted file mode 100644
index 1c830e9..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Branch;
-
-import java.util.List;
-
-/**
- * It holds list of branches and boolean to indicate if it is allowed to add new
- * branches.
- */
-public final class ListBranchesResult {
-  protected boolean noRepository;
-  protected boolean canAdd;
-  protected List<Branch> branches;
-
-  protected ListBranchesResult() {
-  }
-
-  public ListBranchesResult(List<Branch> branches, boolean canAdd,
-      boolean noRepository) {
-    this.branches = branches;
-    this.canAdd = canAdd;
-    this.noRepository = noRepository;
-  }
-
-  public boolean getNoRepository() {
-    return noRepository;
-  }
-
-  public boolean getCanAdd() {
-    return canAdd;
-  }
-
-  public List<Branch> getBranches() {
-    return branches;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index fecbb76..914e69f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -48,6 +48,8 @@
   protected List<Edit> edits;
   protected DisplayMethod displayMethodA;
   protected DisplayMethod displayMethodB;
+  protected transient String mimeTypeA;
+  protected transient String mimeTypeB;
   protected CommentDetail comments;
   protected List<Patch> history;
   protected boolean hugeFile;
@@ -60,8 +62,9 @@
       final List<String> h, final AccountDiffPreference dp,
       final SparseFileContent ca, final SparseFileContent cb,
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
-      final CommentDetail cd, final List<Patch> hist, final boolean hf,
-      final boolean id, final boolean idf, final boolean idt) {
+      final String mta, final String mtb, final CommentDetail cd,
+      final List<Patch> hist, final boolean hf, final boolean id,
+      final boolean idf, final boolean idt) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -75,6 +78,8 @@
     edits = e;
     displayMethodA = ma;
     displayMethodB = mb;
+    mimeTypeA = mta;
+    mimeTypeB = mtb;
     comments = cd;
     history = hist;
     hugeFile = hf;
@@ -170,6 +175,14 @@
     return b;
   }
 
+  public String getMimeTypeA() {
+    return mimeTypeA;
+  }
+
+  public String getMimeTypeB() {
+    return mimeTypeB;
+  }
+
   public List<Edit> getEdits() {
     return edits;
   }
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 39f5cb0..9f4da74 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,6 +19,7 @@
 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 {
@@ -26,6 +27,7 @@
   protected PatchSetInfo info;
   protected List<Patch> patches;
   protected Project.NameKey project;
+  protected List<UiCommandDetail> commands;
 
   public PatchSetDetail() {
   }
@@ -61,4 +63,15 @@
   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-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 43fefba..69263d9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -29,6 +29,7 @@
   public static final String FORGE_COMMITTER = "forgeCommitter";
   public static final String FORGE_SERVER = "forgeServerAsCommitter";
   public static final String LABEL = "label-";
+  public static final String LABEL_AS = "labelAs-";
   public static final String OWNER = "owner";
   public static final String PUBLISH_DRAFTS = "publishDrafts";
   public static final String PUSH = "push";
@@ -43,6 +44,7 @@
 
   private static final List<String> NAMES_LC;
   private static final int labelIndex;
+  private static final int labelAsIndex;
 
   static {
     NAMES_LC = new ArrayList<String>();
@@ -58,6 +60,7 @@
     NAMES_LC.add(PUSH_TAG.toLowerCase());
     NAMES_LC.add(PUSH_SIGNED_TAG.toLowerCase());
     NAMES_LC.add(LABEL.toLowerCase());
+    NAMES_LC.add(LABEL_AS.toLowerCase());
     NAMES_LC.add(REBASE.toLowerCase());
     NAMES_LC.add(REMOVE_REVIEWER.toLowerCase());
     NAMES_LC.add(SUBMIT.toLowerCase());
@@ -67,15 +70,18 @@
     NAMES_LC.add(PUBLISH_DRAFTS.toLowerCase());
 
     labelIndex = NAMES_LC.indexOf(Permission.LABEL);
+    labelAsIndex = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
   }
 
   /** @return true if the name is recognized as a permission name. */
   public static boolean isPermission(String varName) {
-    String lc = varName.toLowerCase();
-    if (lc.startsWith(LABEL)) {
-      return LABEL.length() < lc.length();
-    }
-    return NAMES_LC.contains(lc);
+    return isLabel(varName)
+        || isLabelAs(varName)
+        || NAMES_LC.contains(varName.toLowerCase());
+  }
+
+  public static boolean hasRange(String varName) {
+    return isLabel(varName) || isLabelAs(varName);
   }
 
   /** @return true if the permission name is actually for a review label. */
@@ -83,11 +89,30 @@
     return varName.startsWith(LABEL) && LABEL.length() < varName.length();
   }
 
+  /** @return true if the permission is for impersonated review labels. */
+  public static boolean isLabelAs(String var) {
+    return var.startsWith(LABEL_AS) && LABEL_AS.length() < var.length();
+  }
+
   /** @return permission name for the given review label. */
   public static String forLabel(String labelName) {
     return LABEL + labelName;
   }
 
+  /** @return permission name to apply a label for another user. */
+  public static String forLabelAs(String labelName) {
+    return LABEL_AS + labelName;
+  }
+
+  public static String extractLabel(String varName) {
+    if (isLabel(varName)) {
+      return varName.substring(LABEL.length());
+    } else if (isLabelAs(varName)) {
+      return varName.substring(LABEL_AS.length());
+    }
+    return null;
+  }
+
   public static boolean canBeOnAllProjects(String ref, String permissionName) {
     if (AccessSection.ALL.equals(ref)) {
       return !OWNER.equals(permissionName);
@@ -110,15 +135,8 @@
     return name;
   }
 
-  public boolean isLabel() {
-    return isLabel(getName());
-  }
-
   public String getLabel() {
-    if (isLabel()) {
-      return getName().substring(LABEL.length());
-    }
-    return null;
+    return extractLabel(getName());
   }
 
   public Boolean getExclusiveGroup() {
@@ -223,8 +241,10 @@
   }
 
   private static int index(Permission a) {
-    if (a.isLabel()) {
+    if (isLabel(a.getName())) {
       return labelIndex;
+    } else if (isLabelAs(a.getName())) {
+      return labelAsIndex;
     }
 
     int index = NAMES_LC.indexOf(a.getName().toLowerCase());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
index 3490dd7..0363fd6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
@@ -86,7 +86,7 @@
   }
 
   public String getLabel() {
-    return isLabel() ? getName().substring(Permission.LABEL.length()) : null;
+    return Permission.extractLabel(getName());
   }
 
   public int getMin() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index 904c5c7..ee6cc95 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class ProjectAccess {
@@ -28,6 +29,7 @@
   protected boolean isConfigVisible;
   protected boolean canUpload;
   protected LabelTypes labelTypes;
+  protected Map<String, String> capabilities;
 
   public ProjectAccess() {
   }
@@ -112,4 +114,12 @@
   public void setLabelTypes(LabelTypes labelTypes) {
     this.labelTypes = labelTypes;
   }
+
+  public Map<String, String> getCapabilities() {
+    return capabilities;
+  }
+
+  public void setCapabilities(Map<String, String> capabilities) {
+    this.capabilities = capabilities;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
index 0638906..44f60861 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -25,25 +24,14 @@
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
-import java.util.Set;
 
 @RpcImpl(version = Version.V2_0)
 public interface ProjectAdminService extends RemoteJsonService {
-  void visibleProjectDetails(AsyncCallback<List<ProjectDetail>> callback);
-
-  void projectDetail(Project.NameKey projectName,
-      AsyncCallback<ProjectDetail> callback);
-
   void projectAccess(Project.NameKey projectName,
       AsyncCallback<ProjectAccess> callback);
 
   @Audit
   @SignInRequired
-  void changeProjectSettings(Project update,
-      AsyncCallback<ProjectDetail> callback);
-
-  @Audit
-  @SignInRequired
   void changeProjectAccess(Project.NameKey projectName, String baseRevision,
       String message, List<AccessSection> sections,
       AsyncCallback<ProjectAccess> callback);
@@ -52,17 +40,4 @@
   void reviewProjectAccess(Project.NameKey projectName, String baseRevision,
       String message, List<AccessSection> sections,
       AsyncCallback<Change.Id> callback);
-
-  void listBranches(Project.NameKey projectName,
-      AsyncCallback<ListBranchesResult> callback);
-
-  @Audit
-  @SignInRequired
-  void addBranch(Project.NameKey projectName, String branchName,
-      String startingRevision, AsyncCallback<AddBranchResult> callback);
-
-  @Audit
-  @SignInRequired
-  void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids,
-      AsyncCallback<Set<Branch.NameKey>> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
deleted file mode 100644
index 1e7589b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.InheritedBoolean;
-import com.google.gerrit.reviewdb.client.Project;
-
-public class ProjectDetail {
-  public Project project;
-  public boolean canModifyDescription;
-  public boolean canModifyMergeType;
-  public boolean canModifyAgreements;
-  public boolean canModifyAccess;
-  public boolean canModifyState;
-  public boolean isPermissionOnly;
-  public InheritedBoolean useContributorAgreements;
-  public InheritedBoolean useSignedOffBy;
-  public InheritedBoolean useContentMerge;
-  public InheritedBoolean requireChangeID;
-
-  public ProjectDetail() {
-  }
-
-  public void setProject(final Project p) {
-    project = p;
-  }
-
-  public void setCanModifyDescription(final boolean cmd) {
-    canModifyDescription = cmd;
-  }
-
-  public void setCanModifyMergeType(final boolean cmmt) {
-    canModifyMergeType = cmmt;
-  }
-
-  public void setCanModifyState(final boolean cms) {
-    canModifyState = cms;
-  }
-
-  public void setCanModifyAgreements(final boolean cma) {
-    canModifyAgreements = cma;
-  }
-
-  public void setCanModifyAccess(final boolean cma) {
-    canModifyAccess = cma;
-  }
-
-  public void setPermissionOnly(final boolean ipo) {
-    isPermissionOnly = ipo;
-  }
-
-  public void setUseContributorAgreements(final InheritedBoolean uca) {
-    useContributorAgreements = uca;
-  }
-
-  public void setUseSignedOffBy(final InheritedBoolean usob) {
-    useSignedOffBy = usob;
-  }
-
-  public void setUseContentMerge(final InheritedBoolean ucm) {
-    useContentMerge = ucm;
-  }
-
-  public void setRequireChangeID(final InheritedBoolean rcid) {
-    requireChangeID = rcid;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index 7205b74..2423757 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -30,7 +30,7 @@
       AsyncCallback<List<AccountInfo>> callback);
 
   /**
-   * @see #suggestAccountGroup(com.google.gerrit.reviewdb.client.Project.NameKey, String, int, AsyncCallback)
+   * @see #suggestAccountGroupForProject(com.google.gerrit.reviewdb.client.Project.NameKey, String, int, AsyncCallback)
    */
   @Deprecated
   void suggestAccountGroup(String query, int limit,
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/data/UiCommandDetail.java
new file mode 100644
index 0000000..cd011860
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
@@ -0,0 +1,24 @@
+// 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.common.data;
+
+/** Detail necessary to display an action. */
+public class UiCommandDetail {
+  public String id;
+  public String method;
+  public String label;
+  public String title;
+  public boolean enabled;
+}
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
new file mode 100644
index 0000000..5aa57dd
--- /dev/null
+++ b/gerrit-extension-api/BUCK
@@ -0,0 +1,21 @@
+SRC = 'src/main/java/com/google/gerrit/extensions/'
+
+gwt_module(
+  name = 'client',
+  srcs = glob([SRC + 'webui/GerritTopMenu.java']),
+  gwtxml = SRC + 'Extensions.gwt.xml',
+  visibility = ['PUBLIC'],
+)
+
+java_library2(
+  name = 'api',
+  srcs = glob([SRC + '**/*.java']),
+  compile_deps = ['//lib/guice:guice'],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'api-src',
+  srcs = glob([SRC + '**/*.java']),
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
deleted file mode 100644
index 31fccc0..0000000
--- a/gerrit-extension-api/pom.xml
+++ /dev/null
@@ -1,122 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-extension-api</artifactId>
-  <name>Gerrit Code Review - Extension API</name>
-
-  <description>
-    Interfaces describing the extension API
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.inject</groupId>
-      <artifactId>guice</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.inject.extensions</groupId>
-      <artifactId>guice-servlet</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>servlet-api</artifactId>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>bundle-sources</id>
-            <phase>package</phase>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-shade-plugin</artifactId>
-        <configuration>
-          <createSourcesJar>true</createSourcesJar>
-          <shadedArtifactAttached>true</shadedArtifactAttached>
-          <shadedClassifierName>all</shadedClassifierName>
-        </configuration>
-        <executions>
-          <execution>
-            <phase>package</phase>
-            <goals>
-              <goal>shade</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-antrun-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>unpack-sources</id>
-            <phase>package</phase>
-            <configuration>
-              <tasks>
-                <unzip src="${project.build.directory}/${project.artifactId}-${project.version}-all-sources.jar" dest="${project.build.directory}/unpack_sources" />
-              </tasks>
-            </configuration>
-            <goals>
-              <goal>run</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-javadoc-plugin</artifactId>
-        <configuration>
-          <sourcepath>${project.build.directory}/unpack_sources</sourcepath>
-          <encoding>ISO-8859-1</encoding>
-          <quiet>true</quiet>
-          <detectOfflineLinks>false</detectOfflineLinks>
-        </configuration>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-            <phase>package</phase>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
new file mode 100644
index 0000000..ef6a827
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
@@ -0,0 +1,18 @@
+<!--
+ 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.
+-->
+<module>
+  <source path='webui' />
+</module>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java
new file mode 100644
index 0000000..ede8b8c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java
@@ -0,0 +1,36 @@
+// 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.extensions.annotations;
+
+/** Declared scope of a capability named by {@link RequiresCapability}. */
+public enum CapabilityScope {
+  /**
+   * Scope is assumed based on the context.
+   *
+   * When {@code @RequiresCapability} is used within a plugin the scope of the
+   * capability is assumed to be that plugin.
+   *
+   * If {@code @RequiresCapability} is used within the core Gerrit Code Review
+   * server (and thus is outside of a plugin) the scope is the core server and
+   * will use {@link com.google.gerrit.common.data.GlobalCapability}.
+   */
+  CONTEXT,
+
+  /** Scope is only the plugin. */
+  PLUGIN,
+
+  /** Scope is the core server. */
+  CORE;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
index 382f4ea..ec0b361 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
@@ -21,11 +21,16 @@
 import java.lang.annotation.Target;
 
 /**
- * Annotation on {@link SshCommand} or {@link RestApiServlet} declaring a
+ * Annotation on {@link com.google.gerrit.sshd.SshCommand} or
+ * {@link com.google.gerrit.httpd.restapi.RestApiServlet} declaring a
  * capability must be granted.
  */
 @Target({ElementType.TYPE})
 @Retention(RUNTIME)
 public @interface RequiresCapability {
+  /**  Name of the capability required to invoke this action. */
   String value();
+
+  /** Scope of the named capability. */
+  CapabilityScope scope() default CapabilityScope.CONTEXT;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
new file mode 100644
index 0000000..aafb583
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
@@ -0,0 +1,24 @@
+// 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.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Specifies a capability declared by a plugin. */
+@ExtensionPoint
+public abstract class CapabilityDefinition {
+  /** @return description of the capability. */
+  public abstract String getDescription();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/DownloadCommand.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/DownloadCommand.java
new file mode 100644
index 0000000..83f3fc4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/DownloadCommand.java
@@ -0,0 +1,33 @@
+// 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.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public abstract class DownloadCommand {
+  /**
+   * Returns the download command for the given download scheme, project and
+   * ref.
+   *
+   * @param scheme the download scheme for which the command should be returned
+   * @param project the name of the project for which the download command
+   *        should be returned
+   * @param ref the change ref
+   * @return the download command
+   */
+  public abstract String getCommand(DownloadScheme scheme, String project,
+      String ref);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/DownloadScheme.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/DownloadScheme.java
new file mode 100644
index 0000000..20eda97
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/DownloadScheme.java
@@ -0,0 +1,39 @@
+// 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.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public abstract class DownloadScheme {
+  /**
+   * Returns the URL of this download scheme.
+   *
+   * @param project the name of the project for which the URL should be returned
+   * @return URL of the download scheme
+   */
+  public abstract String getUrl(String project);
+
+  /** @return whether this scheme requires authentication */
+  public abstract boolean isAuthRequired();
+
+  /** @return whether this scheme supports authentication */
+  public boolean isAuthSupported() {
+    return isAuthRequired();
+  }
+
+  /** @return whether the download scheme is enabled */
+  public abstract boolean isEnabled();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
index 2911ded..49a697e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -16,20 +16,15 @@
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 
-import java.util.List;
-
 /** Notified when one or more references are modified. */
 @ExtensionPoint
 public interface GitReferenceUpdatedListener {
-  public interface Update {
-    String getRefName();
-    String getOldObjectId();
-    String getNewObjectId();
-  }
 
   public interface Event {
     String getProjectName();
-    List<Update> getUpdates();
+    String getRefName();
+    String getOldObjectId();
+    String getNewObjectId();
   }
 
   void onGitReferenceUpdated(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java
new file mode 100644
index 0000000..b03f99c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java
@@ -0,0 +1,27 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Notified whenever a project is deleted on the master. */
+@ExtensionPoint
+public interface ProjectDeletedListener {
+  public interface Event {
+    String getProjectName();
+  }
+
+  void onProjectDeleted(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
index 40bbb80..ea4a751 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -17,11 +17,13 @@
 import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.util.Types;
 
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.SortedSet;
@@ -39,7 +41,7 @@
  * internally, and resolve the provider to an instance on demand. This enables
  * registrations to decide between singleton and non-singleton members.
  */
-public abstract class DynamicMap<T> {
+public abstract class DynamicMap<T> implements Iterable<DynamicMap.Entry<T>> {
   /**
    * Declare a singleton {@code DynamicMap<T>} with a binder.
    * <p>
@@ -102,7 +104,7 @@
    * @throws ProvisionException if the registered provider is unable to obtain
    *         an instance of the requested implementation.
    */
-  public T get(String pluginName, String exportName) {
+  public T get(String pluginName, String exportName) throws ProvisionException {
     Provider<T> p = items.get(new NamePair(pluginName, exportName));
     return p != null ? p.get() : null;
   }
@@ -136,6 +138,50 @@
     return Collections.unmodifiableSortedMap(r);
   }
 
+  /** Iterate through all entries in an undefined order. */
+  public Iterator<Entry<T>> iterator() {
+    final Iterator<Map.Entry<NamePair, Provider<T>>> i =
+        items.entrySet().iterator();
+    return new Iterator<Entry<T>>() {
+      @Override
+      public boolean hasNext() {
+        return i.hasNext();
+      }
+
+      @Override
+      public Entry<T> next() {
+        final Map.Entry<NamePair, Provider<T>> e = i.next();
+        return new Entry<T>() {
+          @Override
+          public String getPluginName() {
+            return e.getKey().pluginName;
+          }
+
+          @Override
+          public String getExportName() {
+            return e.getKey().exportName;
+          }
+
+          @Override
+          public Provider<T> getProvider() {
+            return e.getValue();
+          }
+        };
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
+
+  public interface Entry<T> {
+    String getPluginName();
+    String getExportName();
+    Provider<T> getProvider();
+  }
+
   static class NamePair {
     private final String pluginName;
     private final String exportName;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index ec34887..b2f19e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -26,6 +26,7 @@
 import com.google.inject.util.Types;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -127,6 +128,11 @@
     return binder.bind(type).annotatedWith(name);
   }
 
+  public static <T> DynamicSet<T> emptySet() {
+    return new DynamicSet<T>(
+        Collections.<AtomicReference<Provider<T>>> emptySet());
+  }
+
   private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
 
   DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
index 3558794..e930a69 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -51,7 +51,7 @@
    * @param key unique description from the item's Guice binding. This can be
    *        later obtained from the registration handle to facilitate matching
    *        with the new equivalent instance during a hot reload. The key must
-   *        use an {@link @Export} annotation.
+   *        use an {@link Export} annotation.
    * @param item the item to add to the collection right now. Must not be null.
    * @return a handle that can remove this item later, or hot-swap the item
    *         without it ever leaving the collection.
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 188011c..852c56e 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
@@ -61,6 +61,8 @@
   private String characterEncoding;
   private long contentLength = -1;
   private boolean gzip = true;
+  private boolean base64 = false;
+  private String attachmentName;
 
   /** @return the MIME type of the result, for HTTP clients. */
   public String getContentType() {
@@ -88,6 +90,17 @@
     return this;
   }
 
+  /** Get the attachment file name; null if not set. */
+  public String getAttachmentName() {
+    return attachmentName;
+  }
+
+  /** Set the attachment file name and return {@code this}. */
+  public BinaryResult setAttachmentName(String attachmentName) {
+    this.attachmentName = attachmentName;
+    return this;
+  }
+
   /** @return length in bytes of the result; -1 if not known. */
   public long getContentLength() {
     return contentLength;
@@ -110,6 +123,17 @@
     return this;
   }
 
+  /** @return true if the result must be base64 encoded. */
+  public boolean isBase64() {
+    return base64;
+  }
+
+  /** Wrap the binary data in base64 encoding. */
+  public BinaryResult base64() {
+    base64 = true;
+    return this;
+  }
+
   /**
    * Write or copy the result onto the specified output stream.
    *
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
new file mode 100644
index 0000000..72f1bc5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
@@ -0,0 +1,66 @@
+// 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.extensions.restapi;
+
+import java.util.concurrent.TimeUnit;
+
+public class CacheControl {
+
+  public enum Type {
+    NONE, PUBLIC, PRIVATE;
+  }
+
+  public static final CacheControl NONE = new CacheControl(Type.NONE, 0, null);
+
+  public static CacheControl PUBLIC(long age, TimeUnit unit) {
+    return new CacheControl(Type.PUBLIC, age, unit);
+  }
+
+  public static CacheControl PRIVATE(long age, TimeUnit unit) {
+    return new CacheControl(Type.PRIVATE, age, unit);
+  }
+
+  private final Type type;
+  private final long age;
+  private final TimeUnit unit;
+  private boolean mustRevalidate;
+
+  private CacheControl(Type type, long age, TimeUnit unit) {
+    this.type = type;
+    this.age = age;
+    this.unit = unit;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public long getAge() {
+    return age;
+  }
+
+  public TimeUnit getUnit() {
+    return unit;
+  }
+
+  public boolean isMustRevalidate() {
+    return mustRevalidate;
+  }
+
+  public CacheControl setMustRevalidate() {
+    mustRevalidate = true;
+    return this;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
index f987425..f0f7dea 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
@@ -25,6 +25,11 @@
     return new IdString(id);
   }
 
+  /** Construct an identifier from an already decoded string. */
+  public static IdString fromDecoded(String id) {
+    return new IdString(Url.encode(id));
+  }
+
   private final String urlEncoded;
 
   private IdString(String s) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
index 8b0fdd3..61c6345 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
@@ -17,4 +17,13 @@
 /** Method is not acceptable on the resource (HTTP 405 Method Not Allowed). */
 public class MethodNotAllowedException extends RestApiException {
   private static final long serialVersionUID = 1L;
+
+  public MethodNotAllowedException() {
+    super();
+  }
+
+  /** @param msg error text for client describing why the method is not allowed. */
+  public MethodNotAllowedException(String msg) {
+    super(msg);
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java
deleted file mode 100644
index b2d62b3..0000000
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java
+++ /dev/null
@@ -1,26 +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.extensions.restapi;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-
-/** Raw data stream supplied by the body of a PUT. */
-public interface PutInput {
-  String getContentType();
-  long getContentLength();
-  InputStream getInputStream() throws IOException;
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RawInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RawInput.java
new file mode 100644
index 0000000..4f195e4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RawInput.java
@@ -0,0 +1,25 @@
+// 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.extensions.restapi;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Raw data stream supplied by the body of a PUT or POST. */
+public interface RawInput {
+  String getContentType();
+  long getContentLength();
+  InputStream getInputStream() throws IOException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
index eb6d811..aa503c1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
@@ -29,4 +29,9 @@
   public ResourceConflictException(String msg) {
     super(msg);
   }
+
+  /** @param msg message to return to the client describing the error. */
+  public ResourceConflictException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
index aa891c9..76942e6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -28,7 +28,18 @@
   }
 
   /** @param id portion of the resource URI that does not exist. */
+  public ResourceNotFoundException(String id, Throwable cause) {
+    super(id, cause);
+  }
+
+  /** @param id portion of the resource URI that does not exist. */
   public ResourceNotFoundException(IdString id) {
     super(id.get());
   }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public ResourceNotFoundException caching(CacheControl c) {
+    return super.caching(c);
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 97c4cbc..848004d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -50,11 +50,14 @@
 
   public abstract int statusCode();
   public abstract T value();
+  public abstract CacheControl caching();
+  public abstract Response<T> caching(CacheControl c);
   public abstract String toString();
 
   private static final class Impl<T> extends Response<T> {
     private final int statusCode;
     private final T value;
+    private CacheControl caching = CacheControl.NONE;
 
     private Impl(int sc, T val) {
       statusCode = sc;
@@ -72,6 +75,17 @@
     }
 
     @Override
+    public CacheControl caching() {
+      return caching;
+    }
+
+    @Override
+    public Response<T> caching(CacheControl c) {
+      caching = c;
+      return this;
+    }
+
+    @Override
     public String toString() {
       return "[" + statusCode() + "] " + value();
     }
@@ -91,6 +105,16 @@
     }
 
     @Override
+    public CacheControl caching() {
+      return CacheControl.NONE;
+    }
+
+    @Override
+    public Response<Object> caching(CacheControl c) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
     public String toString() {
       return "[204 No Content] None";
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
index a6d27cd..3fae128 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
@@ -17,6 +17,7 @@
 /** Root exception type for JSON API failures. */
 public abstract class RestApiException extends Exception {
   private static final long serialVersionUID = 1L;
+  private CacheControl caching = CacheControl.NONE;
 
   public RestApiException() {
   }
@@ -28,4 +29,14 @@
   public RestApiException(String msg, Throwable cause) {
     super(msg, cause);
   }
+
+  public CacheControl caching() {
+    return caching;
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T extends RestApiException> T caching(CacheControl c) {
+    caching = c;
+    return (T) this;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
index 063a570..8032531 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import java.sql.Timestamp;
+
 /**
  * Generic resource handle defining arguments to views.
  * <p>
@@ -21,4 +23,18 @@
  * {@link RestView} such as {@link RestReadView} or {@link RestModifyView}.
  */
 public interface RestResource {
+
+  /** A resource with a last modification date. */
+  public interface HasLastModified {
+    /**
+     * @return time for the Last-Modified header. HTTP truncates the header
+     *         value to seconds.
+     */
+    public Timestamp getLastModified();
+  }
+
+  /** A resource with an ETag. */
+  public interface HasETag {
+    public String getETag();
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
deleted file mode 100644
index b2fb901..0000000
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.restapi;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-public interface StreamingResponse {
-  public String getContentType();
-  public void stream(OutputStream out) throws IOException;
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GerritTopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GerritTopMenu.java
new file mode 100644
index 0000000..e3821fc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GerritTopMenu.java
@@ -0,0 +1,25 @@
+// 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.extensions.webui;
+
+public enum GerritTopMenu {
+  ALL, MY, DIFFERENCES, PROJECTS, PEOPLE, PLUGINS, DOCUMENTATION;
+
+  public final String menuName;
+
+  private GerritTopMenu() {
+    menuName = name().substring(0, 1) + name().substring(1).toLowerCase();
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PrivateInternals_UiActionDescription.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PrivateInternals_UiActionDescription.java
new file mode 100644
index 0000000..6545db8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PrivateInternals_UiActionDescription.java
@@ -0,0 +1,33 @@
+// 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.extensions.webui;
+
+/**
+ * Internal implementation helper for Gerrit Code Review server.
+ * <p>
+ * Extensions and plugins should not invoke this class.
+ */
+public class PrivateInternals_UiActionDescription {
+  public static void setMethod(UiAction.Description d, String method) {
+    d.setMethod(method);
+  }
+
+  public static void setId(UiAction.Description d, String id) {
+    d.setId(id);
+  }
+
+  private PrivateInternals_UiActionDescription() {
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
new file mode 100644
index 0000000..e5a1f7e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
@@ -0,0 +1,60 @@
+// 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.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.List;
+
+@ExtensionPoint
+public interface TopMenu {
+  public class MenuEntry {
+    public final String name;
+    public final List<MenuItem> items;
+
+    public MenuEntry(GerritTopMenu gerritMenu, List<MenuItem> items) {
+      this(gerritMenu.menuName, items);
+    }
+
+    public MenuEntry(String name, List<MenuItem> items) {
+      this.name = name;
+      this.items = items;
+    }
+  }
+
+  public class MenuItem {
+    public final String url;
+    public final String name;
+    public final String target;
+    public final String id;
+
+    public MenuItem(String name, String url) {
+      this(name, url, "_blank");
+    }
+
+    public MenuItem(String name, String url, String target) {
+      this(name, url, target, null);
+    }
+
+    public MenuItem(String name, String url, String target, String id) {
+      this.url = url;
+      this.name = name;
+      this.target = target;
+      this.id = id;
+    }
+  }
+
+  List<MenuEntry> getEntries();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
new file mode 100644
index 0000000..63172ce
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -0,0 +1,104 @@
+// 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.extensions.webui;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+
+public interface UiAction<R extends RestResource> extends RestView<R> {
+  /**
+   * Get the description of the action customized for the resource.
+   *
+   * @param resource the resource the view would act upon if the action is
+   *        invoked by the client. Information from the resource can be used to
+   *        customize the description.
+   * @return a description of the action. The server will populate the
+   *         {@code id} and {@code method} properties. If null the action will
+   *         assumed unavailable and not presented. This is usually the same as
+   *         {@code setVisible(false)}.
+   */
+  public Description getDescription(R resource);
+
+  /** Describes an action invokable through the web interface. */
+  public static class Description {
+    private String method;
+    private String id;
+    private String label;
+    private String title;
+    private boolean visible = true;
+    private boolean enabled = true;
+
+    public String getMethod() {
+      return method;
+    }
+
+    /** {@code PrivateInternals_UiActionDescription.setMethod()} */
+    void setMethod(String method) {
+      this.method = method;
+    }
+
+    public String getId() {
+      return id;
+    }
+
+    /** {@code PrivateInternals_UiActionDescription.setId()} */
+    void setId(String id) {
+      this.id = id;
+    }
+
+    public String getLabel() {
+      return label;
+    }
+
+    /** Set the label to appear on the button to activate this action. */
+    public Description setLabel(String label) {
+      this.label = label;
+      return this;
+    }
+
+    public String getTitle() {
+      return title;
+    }
+
+    /** Set the tool-tip text to appear when the mouse hovers on the button. */
+    public Description setTitle(String title) {
+      this.title = title;
+      return this;
+    }
+
+    public boolean isVisible() {
+      return visible;
+    }
+
+    /**
+     * Set if the action's button is visible on screen for the current client.
+     * If not visible the action description may not be sent to the client.
+     */
+    public Description setVisible(boolean visible) {
+      this.visible = visible;
+      return this;
+    }
+
+    public boolean isEnabled() {
+      return enabled && isVisible();
+    }
+
+    /** Set if the button should be invokable (true), or greyed out (false). */
+    public Description setEnabled(boolean enabled) {
+      this.enabled = enabled;
+      return this;
+    }
+  }
+}
diff --git a/gerrit-gwtdebug/BUCK b/gerrit-gwtdebug/BUCK
new file mode 100644
index 0000000..308c2f1
--- /dev/null
+++ b/gerrit-gwtdebug/BUCK
@@ -0,0 +1,6 @@
+java_library(
+  name = 'gwtdebug',
+  srcs = ['src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java'],
+  deps = ['//lib/gwt:dev'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
deleted file mode 100644
index 83c10eb..0000000
--- a/gerrit-gwtdebug/pom.xml
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-gwtdebug</artifactId>
-  <name>Gerrit Code Review - GWT UI Debugging Support</name>
-
-  <description>
-    Debugging support for the GWT UI
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-gwtui</artifactId>
-      <version>${project.version}</version>
-      <classifier>classes</classifier>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-war</artifactId>
-      <version>${project.version}</version>
-      <classifier>classes</classifier>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.gerrit</groupId>
-          <artifactId>gerrit-pgm</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.h2database</groupId>
-      <artifactId>h2</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>postgresql</groupId>
-      <artifactId>postgresql</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>bouncycastle</groupId>
-      <artifactId>bcprov-jdk15</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>bouncycastle</groupId>
-      <artifactId>bcpg-jdk15</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <!-- GWT should require these itself, but doesn't. -->
-    <dependency>
-      <groupId>javax.validation</groupId>
-      <artifactId>validation-api</artifactId>
-      <scope>provided</scope>
-    </dependency>
-    <dependency>
-      <groupId>javax.validation</groupId>
-      <artifactId>validation-api</artifactId>
-      <classifier>sources</classifier>
-      <scope>provided</scope>
-    </dependency>
-
-     <!-- Workaround for overwriting our dependencies (like args4j) by additional
-      classes put in gwt-dev.jar -->
-    <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-dev</artifactId>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
index d23aa35..a2a770e 100644
--- a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
+++ b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
@@ -381,9 +381,15 @@
     Server server = new Server();
     server.addConnector(connector);
 
-    // warDir is "$top/gerrit-gwtui/target/gwt-hosted-mode"
-    //
-    File top = warDir.getParentFile().getParentFile().getParentFile();
+    File top;
+    String root = System.getProperty("gerrit.source_root");
+    if (root != null) {
+      top = new File(root);
+    } else {
+      // Under Maven warDir is "$top/gerrit-gwtui/target/gwt-hosted-mode"
+      top = warDir.getParentFile().getParentFile().getParentFile();
+    }
+
     File app = new File(top, "gerrit-war/src/main/webapp");
     File webxml = new File(app, "WEB-INF/web.xml");
 
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
new file mode 100644
index 0000000..105ee7d
--- /dev/null
+++ b/gerrit-gwtexpui/BUCK
@@ -0,0 +1,104 @@
+SRC = 'src/main/java/com/google/gwtexpui/'
+
+gwt_module(
+  name = 'Clippy',
+  srcs = glob([SRC + 'clippy/client/*.java']),
+  gwtxml = SRC + 'clippy/Clippy.gwt.xml',
+  resources = [
+    SRC + 'clippy/client/clippy.css',
+    SRC + 'clippy/client/clippy.swf',
+  ],
+  deps = [
+    ':SafeHtml',
+    ':UserAgent',
+    '//lib/gwt:user',
+    '//lib:LICENSE-clippy',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+gwt_module(
+  name = 'CSS',
+  srcs = glob([SRC + 'css/rebind/*.java']),
+  gwtxml = SRC + 'css/CSS.gwt.xml',
+  deps = ['//lib/gwt:dev'],
+  visibility = ['PUBLIC'],
+)
+
+gwt_module(
+  name = 'GlobalKey',
+  srcs = glob([SRC + 'globalkey/client/*.java']),
+  gwtxml = SRC + 'globalkey/GlobalKey.gwt.xml',
+  resources = [
+    SRC + 'globalkey/client/KeyConstants.properties',
+    SRC + 'globalkey/client/key.css',
+  ],
+  deps = [
+    ':SafeHtml',
+    ':UserAgent',
+    '//lib/gwt:user',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+gwt_module(
+  name = 'Linker',
+  srcs = glob([SRC + 'linker/rebind/*.java']),
+  gwtxml = SRC + 'linker/ServerPlannedIFrameLinker.gwt.xml',
+  deps = ['//lib/gwt:dev'],
+  visibility = ['PUBLIC'],
+)
+
+java_library2(
+  name = 'linker_server',
+  srcs = glob([SRC + 'linker/server/*.java']),
+  compile_deps = ['//lib:servlet-api-3_0'],
+  visibility = ['PUBLIC'],
+)
+
+gwt_module(
+  name = 'Progress',
+  srcs = glob([SRC + 'progress/client/*.java']),
+  gwtxml = SRC + 'progress/Progress.gwt.xml',
+  resources = [SRC + 'progress/client/progress.css'],
+  deps = ['//lib/gwt:user'],
+  visibility = ['PUBLIC'],
+)
+
+gwt_module(
+  name = 'SafeHtml',
+  srcs = glob([SRC + 'safehtml/client/*.java']),
+  gwtxml = SRC + 'safehtml/SafeHtml.gwt.xml',
+  resources = [SRC + 'safehtml/client/safehtml.css'],
+  deps = ['//lib/gwt:user'],
+  visibility = ['PUBLIC'],
+)
+
+java_test(
+  name = 'SafeHtml_tests',
+  srcs = glob([
+    'src/test/java/com/google/gwtexpui/safehtml/client/**/*.java',
+  ]),
+  deps = [
+    ':SafeHtml',
+    '//lib:junit',
+    '//lib/gwt:user',
+    '//lib/gwt:dev',
+  ],
+  source_under_test = [':SafeHtml'],
+)
+
+gwt_module(
+  name = 'UserAgent',
+  srcs = glob([SRC + 'user/client/*.java']),
+  gwtxml = SRC + 'user/User.gwt.xml',
+  deps = ['//lib/gwt:user'],
+  visibility = ['PUBLIC'],
+)
+
+java_library2(
+  name = 'server',
+  srcs = glob([SRC + 'server/*.java']),
+  compile_deps = ['//lib:servlet-api-3_0'],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-gwtexpui/pom.xml b/gerrit-gwtexpui/pom.xml
deleted file mode 100644
index 2c5ec62..0000000
--- a/gerrit-gwtexpui/pom.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-gwtexpui</artifactId>
-
-  <name>Gerrit Code Review - GWT expui</name>
-  <description>Extended UI tools for GWT</description>
-
-  <build>
-    <plugins>
-      <plugin>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-
-    <extensions>
-      <extension>
-        <groupId>com.googlesource.gerrit</groupId>
-        <artifactId>gs-maven-wagon</artifactId>
-        <version>3.3</version>
-      </extension>
-    </extensions>
-  </build>
-
-  <dependencies>
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <scope>test</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-user</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-dev</artifactId>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
index 4c2b8981..dfa7679 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -16,10 +16,16 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
 
 public interface ClippyResources extends ClientBundle {
   public static final ClippyResources I = GWT.create(ClippyResources.class);
 
   @Source("clippy.css")
   ClippyCss css();
+
+  @Source("clippy.swf")
+  @DoNotEmbed
+  DataResource swf();
 }
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 273318b..c4f887e 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
@@ -14,7 +14,6 @@
 
 package com.google.gwtexpui.clippy.client;
 
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
@@ -47,7 +46,6 @@
 public class CopyableLabel extends Composite implements HasText {
   private static final int SWF_WIDTH = 110;
   private static final int SWF_HEIGHT = 14;
-  private static String swfUrl;
   private static boolean flashEnabled = true;
 
   static {
@@ -63,10 +61,7 @@
   }
 
   private static String swfUrl() {
-    if (swfUrl == null) {
-      swfUrl = GWT.getModuleBaseURL() + "gwtexpui_clippy1.cache.swf";
-    }
-    return swfUrl;
+    return ClippyResources.I.swf().getSafeUri().asString();
   }
 
   private final FlowPanel content;
@@ -76,6 +71,10 @@
   private TextBox textBox;
   private Element swf;
 
+  public CopyableLabel() {
+    this("");
+  }
+
   /**
    * Create a new label
    *
@@ -122,12 +121,11 @@
   public void setPreviewText(final String text) {
     if (textLabel != null) {
       textLabel.setText(text);
-      visibleLen = text.length();
     }
   }
 
   private void embedMovie() {
-    if (flashEnabled && UserAgent.hasFlash) {
+    if (flashEnabled && UserAgent.hasFlash && text.length() > 0) {
       final String flashVars = "text=" + URL.encodeQueryString(getText());
       final SafeHtmlBuilder h = new SafeHtmlBuilder();
 
@@ -154,7 +152,7 @@
       h.closeElement("div");
 
       if (swf != null) {
-        DOM.removeChild(getElement(), swf);
+        getElement().removeChild(swf);
       }
       DOM.appendChild(getElement(), swf = SafeHtml.parse(h));
     }
@@ -183,6 +181,7 @@
       textBox = new TextBox();
       textBox.setText(getText());
       textBox.setVisibleLength(visibleLen);
+      textBox.setReadOnly(true);
       textBox.addKeyPressHandler(new KeyPressHandler() {
         @Override
         public void onKeyPress(final KeyPressEvent event) {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.swf
similarity index 100%
rename from gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
rename to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.swf
Binary files differ
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
index d680a72..c59a4ea 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
@@ -18,16 +18,20 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.Node;
 import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.HasMouseMoveHandlers;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwt.user.client.ui.Widget;
 
-class DocWidget extends Widget implements HasKeyPressHandlers {
+public class DocWidget extends Widget
+    implements HasKeyPressHandlers, HasMouseMoveHandlers {
   private static DocWidget me;
 
-  static DocWidget get() {
+  public static DocWidget get() {
     if (me == null) {
       me = new DocWidget();
     }
@@ -45,6 +49,11 @@
     return addDomHandler(handler, KeyPressEvent.getType());
   }
 
+  @Override
+  public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
+    return addDomHandler(handler, MouseMoveEvent.getType());
+  }
+
   private static Node docnode() {
     return Document.get();
   }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
index 1eaaa3c..daf5d61 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
@@ -15,6 +15,8 @@
 package com.google.gwtexpui.globalkey.client;
 
 import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.event.logical.shared.CloseEvent;
@@ -92,6 +94,14 @@
     active = new State(panel);
     active.add(new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, panel));
     panel.addCloseHandler(restoreGlobal);
+    panel.addDomHandler(new KeyDownHandler() {
+      @Override
+      public void onKeyDown(KeyDownEvent event) {
+        if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+          panel.hide();
+        }
+      }
+    }, KeyDownEvent.getType());
   }
 
   public static HandlerRegistration addApplication(final Widget widget,
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
index ba4f626..032db65 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
@@ -24,6 +24,7 @@
   public static final int M_CTRL = 1 << 16;
   public static final int M_ALT = 2 << 16;
   public static final int M_META = 4 << 16;
+  public static final int M_SHIFT = 8 << 16;
 
   public static boolean same(final KeyCommand a, final KeyCommand b) {
     return a.getClass() == b.getClass() && a.helpText.equals(b.helpText);
@@ -58,6 +59,9 @@
     if ((keyMask & M_META) == M_META) {
       modifier(b, KeyConstants.I.keyMeta());
     }
+    if ((keyMask & M_SHIFT) == M_SHIFT) {
+      modifier(b, KeyConstants.I.keyShift());
+    }
 
     final char c = (char) (keyMask & 0xffff);
     switch (c) {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
index 4f3205a..ad4c23e 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -121,7 +121,10 @@
   }
 
   static int toMask(final KeyPressEvent event) {
-    int mask = event.getCharCode();
+    int mask = event.getUnicodeCharCode();
+    if (mask == 0) {
+      mask = event.getNativeEvent().getKeyCode();
+    }
     if (event.isAltKeyDown()) {
       mask |= KeyCommand.M_ALT;
     }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
index 56fb85c..b4cb41e 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
@@ -32,6 +32,7 @@
   String keyCtrl();
   String keyAlt();
   String keyMeta();
+  String keyShift();
   String keyEnter();
   String keyEsc();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
index e21daf5..2e12b07 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
@@ -10,5 +10,6 @@
 keyCtrl = Ctrl
 keyAlt = Alt
 keyMeta = Meta
+keyShift = Shift
 keyEnter = Enter
 keyEsc = Esc
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
index 7bd0233..c81f871 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -16,8 +16,11 @@
 
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -38,7 +41,7 @@
 
 
 public class KeyHelpPopup extends PluginSafePopupPanel implements
-    KeyPressHandler {
+    KeyPressHandler, KeyDownHandler {
   private final FocusPanel focus;
 
   public KeyHelpPopup() {
@@ -74,9 +77,10 @@
     body.add(lists);
 
     focus = new FocusPanel(body);
-    DOM.setStyleAttribute(focus.getElement(), "outline", "0px");
-    DOM.setElementAttribute(focus.getElement(), "hideFocus", "true");
+    focus.getElement().getStyle().setProperty("outline", "0px");
+    focus.getElement().setAttribute("hideFocus", "true");
     focus.addKeyPressHandler(this);
+    focus.addKeyDownHandler(this);
     add(focus);
   }
 
@@ -100,6 +104,13 @@
     hide();
   }
 
+  @Override
+  public void onKeyDown(final KeyDownEvent event) {
+    if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+      hide();
+    }
+  }
+
   private void populate(final Grid lists) {
     int end[] = new int[5];
     int column = 0;
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
new file mode 100644
index 0000000..7ae7fd0
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
@@ -0,0 +1,24 @@
+// 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.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class NpFlowPanel extends FlowPanel {
+  public NpFlowPanel() {
+    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
index c06d2c4..38a488e 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
@@ -15,7 +15,6 @@
 package com.google.gwtexpui.globalkey.client;
 
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.TextArea;
 
 public class NpTextArea extends TextArea {
@@ -29,6 +28,6 @@
   }
 
   public void setSpellCheck(boolean spell) {
-    DOM.setElementPropertyBoolean(getElement(), "spellcheck", spell);
+    getElement().setPropertyBoolean("spellcheck", spell);
   }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
index e2c576b..61bec18 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -79,7 +79,18 @@
       if (!html) {
         ds = escape(ds);
       }
-      displayString = sgi(ds, qstr, "<strong>$1</strong>");
+
+      // We now surround qstr by <strong>. But the chosen approach is not too
+      // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
+      // escapes (e.g.: "Tim &lt;email@example.org&gt;"). Those escapes will
+      // get <strong>-ed as well (e.g.: "&lt;" -> "&<strong>l</strong>t;"). But
+      // as repairing those mangled escapes is easier than not mangling them in
+      // the first place, we repair them afterwards.
+      ds = sgi(ds, qstr, "<strong>$1</strong>");
+      // Repairing <strong>-ed escapes.
+      ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3");
+
+      displayString = ds;
     }
 
     private static native String sgi(String inString, String pat, String newHtml)
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
index eaa4f23..7e32ff0 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
@@ -47,9 +47,9 @@
   }
 
   /**
-   * @param regex regular expression pattern to match substrings with.
-   * @param repl replacement link href. Capture groups within
-   *        <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+   * @param find regular expression pattern to match substrings with.
+   * @param link replacement link href. Capture groups within
+   *        <code>find</code> can be referenced with <code>$<i>n</i></code>.
    */
   public LinkFindReplace(String find, String link) {
     this.pat = RegExp.compile(find);
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
index d22fef6..63b5fde 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
@@ -29,9 +29,9 @@
   }
 
   /**
-   * @param regex regular expression pattern to match substrings with.
-   * @param repl replacement expression. Capture groups within
-   *        <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+   * @param find regular expression pattern to match substrings with.
+   * @param replace replacement expression. Capture groups within
+   *        <code>find</code> can be referenced with <code>$<i>n</i></code>.
    */
   public RawFindReplace(String find, String replace) {
     this.pat = RegExp.compile(find);
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
index 0a9f7a2..f752780 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -15,10 +15,10 @@
 package com.google.gwtexpui.safehtml.client;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.regexp.shared.MatchResult;
 import com.google.gwt.regexp.shared.RegExp;
 import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.HTMLTable;
 import com.google.gwt.user.client.ui.HasHTML;
@@ -29,7 +29,9 @@
 import java.util.List;
 
 /** Immutable string safely placed as HTML without further escaping. */
-public abstract class SafeHtml {
+@SuppressWarnings("serial")
+public abstract class SafeHtml
+    implements com.google.gwt.safehtml.shared.SafeHtml {
   public static final SafeHtmlResources RESOURCES;
 
   static {
@@ -84,13 +86,13 @@
   }
 
   /** @return the existing inner HTML of any element. */
-  public static SafeHtml get(final Element e) {
-    return new SafeHtmlString(DOM.getInnerHTML(e));
+  public static SafeHtml get(Element e) {
+    return new SafeHtmlString(e.getInnerHTML());
   }
 
   /** Set the inner HTML of any element. */
-  public static Element set(final Element e, final SafeHtml str) {
-    DOM.setInnerHTML(e, str.asString());
+  public static Element setInnerHTML(Element e, SafeHtml str) {
+    e.setInnerHTML(str.asString());
     return e;
   }
 
@@ -107,8 +109,10 @@
   }
 
   /** Parse an HTML block and return the first (typically root) element. */
-  public static Element parse(final SafeHtml str) {
-    return DOM.getFirstChild(set(DOM.createDiv(), str));
+  public static com.google.gwt.user.client.Element parse(SafeHtml html) {
+    com.google.gwt.user.client.Element e = DOM.createDiv();
+    setInnerHTML(e, html);
+    return DOM.getFirstChild(e);
   }
 
   /** Convert bare http:// and https:// URLs into &lt;a href&gt; tags. */
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
index 9fe3267..287988f 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -19,6 +19,7 @@
 /**
  * Safely constructs a {@link SafeHtml}, escaping user provided content.
  */
+@SuppressWarnings("serial")
 public class SafeHtmlBuilder extends SafeHtml {
   private static final Impl impl;
 
@@ -105,6 +106,14 @@
   }
 
   /** Append already safe HTML as-is, avoiding double escaping. */
+  public SafeHtmlBuilder append(com.google.gwt.safehtml.shared.SafeHtml in) {
+    if (in != null) {
+      cb.append(in.asString());
+    }
+    return this;
+  }
+
+  /** Append already safe HTML as-is, avoiding double escaping. */
   public SafeHtmlBuilder append(final SafeHtml in) {
     if (in != null) {
       cb.append(in.asString());
@@ -317,6 +326,16 @@
     return closeElement("td");
   }
 
+  /** Append "&lt;th&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openTh() {
+    return openElement("th");
+  }
+
+  /** Append "&lt;/th&gt;" */
+  public SafeHtmlBuilder closeTh() {
+    return closeElement("th");
+  }
+
   /** Append "&lt;div&gt;"; attributes may be set if needed */
   public SafeHtmlBuilder openDiv() {
     return openElement("div");
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
index a229421..57392bf 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
@@ -14,6 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
+@SuppressWarnings("serial")
 class SafeHtmlString extends SafeHtml {
   private final String html;
 
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
index c4d681f..2033c62 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
@@ -72,17 +72,14 @@
 
   private static boolean cacheForever(final String pathInfo,
       final HttpServletRequest req) {
-    if (pathInfo.endsWith(".cache.html")) {
-      return true;
-    } else if (pathInfo.endsWith(".cache.gif")) {
-      return true;
-    } else if (pathInfo.endsWith(".cache.png")) {
-      return true;
-    } else if (pathInfo.endsWith(".cache.css")) {
-      return true;
-    } else if (pathInfo.endsWith(".cache.jar")) {
-      return true;
-    } else if (pathInfo.endsWith(".cache.swf")) {
+    if (pathInfo.endsWith(".cache.html")
+        || pathInfo.endsWith(".cache.gif")
+        || pathInfo.endsWith(".cache.png")
+        || pathInfo.endsWith(".cache.css")
+        || pathInfo.endsWith(".cache.jar")
+        || pathInfo.endsWith(".cache.swf")
+        || pathInfo.endsWith(".cache.txt")
+        || pathInfo.endsWith(".cache.js")) {
       return true;
     } else if (pathInfo.endsWith(".nocache.js")) {
       final String v = req.getParameter("content");
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
index 11409e8..d44405f 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -59,10 +59,34 @@
   public static void setCacheable(
       HttpServletRequest req, HttpServletResponse res,
       long age, TimeUnit unit) {
+    setCacheable(req, res, age, unit, false);
+  }
+
+  /**
+   * Permit caching the response for up to the age specified.
+   * <p>
+   * If the request is on a secure connection (e.g. SSL) private caching is
+   * used. This allows the user-agent to cache the response, but requests
+   * intermediate proxies to not cache. This may offer better protection for
+   * Set-Cookie headers.
+   * <p>
+   * If the request is on plaintext (insecure), public caching is used. This may
+   * allow an intermediate proxy to cache the response, including any Set-Cookie
+   * header that may have also been included.
+   *
+   * @param req current request.
+   * @param res response being returned.
+   * @param age how long the response can be cached.
+   * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   * @param mustRevalidate true if the client must validate the cached entity.
+   */
+  public static void setCacheable(
+      HttpServletRequest req, HttpServletResponse res,
+      long age, TimeUnit unit, boolean mustRevalidate) {
     if (req.isSecure()) {
-      setCacheablePrivate(res, age, unit);
+      setCacheablePrivate(res, age, unit, mustRevalidate);
     } else {
-      setCacheablePublic(res, age, unit);
+      setCacheablePublic(res, age, unit, mustRevalidate);
     }
   }
 
@@ -76,15 +100,16 @@
    * @param res response being returned.
    * @param age how long the response can be cached.
    * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   * @param mustRevalidate true if the client must validate the cached entity.
    */
   public static void setCacheablePublic(HttpServletResponse res,
-      long age, TimeUnit unit) {
+      long age, TimeUnit unit, boolean mustRevalidate) {
     long now = System.currentTimeMillis();
     long sec = maxAgeSeconds(age, unit);
 
     res.setDateHeader("Expires", now + SECONDS.toMillis(sec));
     res.setDateHeader("Date", now);
-    cache(res, "public", age, unit);
+    cache(res, "public", age, unit, mustRevalidate);
   }
 
   /**
@@ -93,20 +118,22 @@
    * @param res response being returned.
    * @param age how long the response can be cached.
    * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   * @param mustRevalidate true if the client must validate the cached entity.
    */
   public static void setCacheablePrivate(HttpServletResponse res,
-      long age, TimeUnit unit) {
+      long age, TimeUnit unit, boolean mustRevalidate) {
     long now = System.currentTimeMillis();
     res.setDateHeader("Expires", now);
     res.setDateHeader("Date", now);
-    cache(res, "private", age, unit);
+    cache(res, "private", age, unit, mustRevalidate);
   }
 
   private static void cache(HttpServletResponse res,
-      String type, long age, TimeUnit unit) {
+      String type, long age, TimeUnit unit, boolean revalidate) {
     res.setHeader("Cache-Control", String.format(
-        "%s, max-age=%d",
-        type, maxAgeSeconds(age, unit)));
+        "%s, max-age=%d%s",
+        type, maxAgeSeconds(age, unit),
+        revalidate ? ", must-revalidate" : ""));
   }
 
   private static long maxAgeSeconds(long age, TimeUnit unit) {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
index c681d89..f4f8d51 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
@@ -15,13 +15,4 @@
 -->
 <module>
   <inherits name="com.google.gwt.user.User"/>
-
-  <replace-with class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImplAutoHide">
-    <when-type-is class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImpl" />
-    <any>
-      <when-property-is name="user.agent" value="safari"/>
-      <when-property-is name="user.agent" value="gecko"/>
-      <when-property-is name="user.agent" value="gecko1_8"/>
-    </any>
-  </replace-with>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java
new file mode 100644
index 0000000..80a940a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java
@@ -0,0 +1,60 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.user.client.ui.Widget;
+
+public class DialogVisibleEvent extends GwtEvent<DialogVisibleHandler> {
+  private static Type<DialogVisibleHandler> TYPE;
+
+  public static Type<DialogVisibleHandler> getType() {
+    if (TYPE == null) {
+      TYPE = new Type<DialogVisibleHandler>();
+    }
+    return TYPE;
+  }
+
+  private final Widget parent;
+  private final boolean visible;
+
+  DialogVisibleEvent(Widget w, boolean visible) {
+    this.parent = w;
+    this.visible = visible;
+  }
+
+  public boolean contains(Widget c) {
+    for (; c != null; c = c.getParent()) {
+      if (c == parent) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public boolean isVisible() {
+    return visible;
+  }
+
+  @Override
+  public Type<DialogVisibleHandler> getAssociatedType() {
+    return getType();
+  }
+
+  @Override
+  protected void dispatch(DialogVisibleHandler handler) {
+    handler.onDialogVisible(this);
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleHandler.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleHandler.java
new file mode 100644
index 0000000..d242db6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleHandler.java
@@ -0,0 +1,21 @@
+// 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.gwtexpui.user.client;
+
+import com.google.gwt.event.shared.EventHandler;
+
+public interface DialogVisibleHandler extends EventHandler {
+  public void onDialogVisible(DialogVisibleEvent event);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
index c6ab09a..80bfba1 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
@@ -14,7 +14,6 @@
 
 package com.google.gwtexpui.user.client;
 
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.ui.DialogBox;
 
 /**
@@ -30,9 +29,6 @@
  * prior setting when the dialog is hidden.
  * */
 public class PluginSafeDialogBox extends DialogBox {
-  private final PluginSafeDialogBoxImpl impl =
-      GWT.create(PluginSafeDialogBoxImpl.class);
-
   public PluginSafeDialogBox() {
     this(false);
   }
@@ -47,19 +43,19 @@
 
   @Override
   public void setVisible(final boolean show) {
-    impl.visible(show);
+    UserAgent.fireDialogVisible(this, show);
     super.setVisible(show);
   }
 
   @Override
   public void show() {
-    impl.visible(true);
+    UserAgent.fireDialogVisible(this, true);
     super.show();
   }
 
   @Override
   public void hide(final boolean autoClosed) {
-    impl.visible(false);
+    UserAgent.fireDialogVisible(this, false);
     super.hide(autoClosed);
   }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
deleted file mode 100644
index a32fc99..0000000
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.user.client;
-
-class PluginSafeDialogBoxImpl {
-  void visible(final boolean dialogVisible) {
-  }
-}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
deleted file mode 100644
index e32fe78..0000000
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.user.client;
-
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NodeList;
-import com.google.gwt.user.client.ui.UIObject;
-
-import java.util.ArrayList;
-
-class PluginSafeDialogBoxImplAutoHide extends PluginSafeDialogBoxImpl {
-  private boolean hidden;
-  private ArrayList<HiddenElement> hiddenElements =
-      new ArrayList<HiddenElement>();
-
-  @Override
-  void visible(final boolean dialogVisible) {
-    if (dialogVisible) {
-      hideAll();
-    } else {
-      showAll();
-    }
-  }
-
-  private void hideAll() {
-    if (!hidden) {
-      hideSet(Document.get().getElementsByTagName("object"));
-      hideSet(Document.get().getElementsByTagName("embed"));
-      hideSet(Document.get().getElementsByTagName("applet"));
-      hidden = true;
-    }
-  }
-
-  private void hideSet(final NodeList<Element> all) {
-    for (int i = 0; i < all.getLength(); i++) {
-      final Element e = all.getItem(i);
-      if (UIObject.isVisible(e)) {
-        hiddenElements.add(new HiddenElement(e));
-      }
-    }
-  }
-
-  private void showAll() {
-    if (hidden) {
-      for (final HiddenElement e : hiddenElements) {
-        e.restore();
-      }
-      hiddenElements.clear();
-      hidden = false;
-    }
-  }
-
-  private static class HiddenElement {
-    private final Element element;
-    private final String visibility;
-
-    HiddenElement(final Element element) {
-      this.element = element;
-      this.visibility = getVisibility(element);
-      setVisibility(element, "hidden");
-    }
-
-    void restore() {
-      setVisibility(element, visibility);
-    }
-
-    private static native String getVisibility(Element elem)
-    /*-{ return elem.style.visibility; }-*/;
-
-    private static native void setVisibility(Element elem, String disp)
-    /*-{ elem.style.visibility = disp; }-*/;
-  }
-}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
index 7d9c9fc..1ed8f99 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
@@ -14,7 +14,6 @@
 
 package com.google.gwtexpui.user.client;
 
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.ui.PopupPanel;
 
 /**
@@ -30,9 +29,6 @@
  * prior setting when the dialog is hidden.
  * */
 public class PluginSafePopupPanel extends PopupPanel {
-  private final PluginSafeDialogBoxImpl impl =
-      GWT.create(PluginSafeDialogBoxImpl.class);
-
   public PluginSafePopupPanel() {
     this(false);
   }
@@ -47,19 +43,19 @@
 
   @Override
   public void setVisible(final boolean show) {
-    impl.visible(show);
+    UserAgent.fireDialogVisible(this, show);
     super.setVisible(show);
   }
 
   @Override
   public void show() {
-    impl.visible(true);
+    UserAgent.fireDialogVisible(this, true);
     super.show();
   }
 
   @Override
   public void hide(final boolean autoClosed) {
-    impl.visible(false);
+    UserAgent.fireDialogVisible(this, false);
     super.hide(autoClosed);
   }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
index 02ba9ae..c654902 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -15,7 +15,11 @@
 package com.google.gwtexpui.user.client;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.SimpleEventBus;
 import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Widget;
 
 /**
  * User agent feature tests we don't create permutations for.
@@ -29,6 +33,16 @@
 public class UserAgent {
   /** Does the browser have ShockwaveFlash plugin enabled? */
   public static final boolean hasFlash = hasFlash();
+  private static final EventBus bus = new SimpleEventBus();
+
+  public static HandlerRegistration addDialogVisibleHandler(
+      DialogVisibleHandler handler) {
+    return bus.addHandler(DialogVisibleEvent.getType(), handler);
+  }
+
+  static void fireDialogVisible(Widget w, boolean visible) {
+    bus.fireEvent(new DialogVisibleEvent(w, visible));
+  }
 
   private static native boolean hasFlash()
   /*-{
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
new file mode 100644
index 0000000..1ae823e
--- /dev/null
+++ b/gerrit-gwtui/BUCK
@@ -0,0 +1,131 @@
+include_defs('//gerrit-gwtui/gwt.defs')
+
+genrule(
+  name = 'ui_optdbg',
+  cmd = 'cd $TMP;' +
+    'unzip -q $SRCDIR/ui_dbg.zip;' +
+    'mv' +
+    ' gerrit_ui/gerrit_ui.nocache.js' +
+    ' gerrit_ui/gerrit_dbg.nocache.js;' +
+    'unzip -qo $SRCDIR/ui_opt.zip;' +
+    'mkdir -p $(dirname $OUT);' +
+    'zip -qr $OUT .',
+  srcs = [
+    genfile('ui_dbg.zip'),
+    genfile('ui_opt.zip'),
+  ],
+  deps = [
+    ':ui_dbg',
+    ':ui_opt',
+  ],
+  out = 'ui_optdbg.zip',
+  visibility = ['PUBLIC'],
+)
+
+gwt_application(
+  name = 'ui_opt',
+  module_target = MODULE,
+  compiler_opts = [
+    '-strict',
+    '-style', 'OBF',
+    '-optimize', '9',
+    '-XdisableClassMetadata',
+    '-XdisableCastChecking',
+  ],
+  deps = APP_DEPS,
+)
+
+gwt_application(
+  name = 'ui_dbg',
+  module_target = MODULE,
+  compiler_opts = DEBUG_OPTS + ['-strict'],
+  deps = APP_DEPS,
+  visibility = ['//:eclipse'],
+)
+
+gwt_user_agent_permutations(
+  name = 'ui',
+  module_name = 'gerrit_ui',
+  module_target = MODULE,
+  compiler_opts = DEBUG_OPTS + ['-draftCompile'],
+  browsers = BROWSERS,
+  deps = APP_DEPS,
+  visibility = ['//:'],
+)
+
+DIFFY = glob(['src/main/java/com/google/gerrit/client/diffy*.png'])
+
+gwt_module(
+  name = 'ui_module',
+  srcs = glob(['src/main/java/**/*.java']),
+  gwtxml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
+  resources = glob(['src/main/java/**/*'], excludes = DIFFY),
+  deps = [
+    ':diffy_logo',
+    '//gerrit-gwtexpui:Clippy',
+    '//gerrit-gwtexpui:CSS',
+    '//gerrit-gwtexpui:GlobalKey',
+    '//gerrit-gwtexpui:Linker',
+    '//gerrit-gwtexpui:Progress',
+    '//gerrit-gwtexpui:SafeHtml',
+    '//gerrit-gwtexpui:UserAgent',
+    '//gerrit-common:client',
+    '//gerrit-extension-api:client',
+    '//gerrit-patch-jgit:client',
+    '//gerrit-prettify:client',
+    '//gerrit-reviewdb:client',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtjsonrpc_src',
+    '//lib:gwtorm',
+    '//lib/codemirror:codemirror',
+    '//lib/gwt:user',
+    '//lib/jgit:jgit',
+  ],
+  visibility = [
+    '//tools/eclipse:classpath',
+    '//Documentation:licenses.txt',
+  ],
+)
+
+prebuilt_jar(
+  name = 'diffy_logo',
+  binary_jar = genfile('diffy_images.jar'),
+  deps = [
+    '//lib:LICENSE-diffy',
+    '//lib:LICENSE-CC-BY3.0',
+    ':diffy_image_files_ln',
+  ],
+)
+
+genrule(
+  name = 'diffy_image_files_ln',
+  cmd = 'ln -s $(location :diffy_image_files) $OUT',
+  deps = [':diffy_image_files'],
+  out = 'diffy_images.jar',
+)
+
+java_library(
+  name = 'diffy_image_files',
+  resources = DIFFY,
+)
+
+java_test(
+  name = 'ui_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  resources = glob(['src/test/resources/**/*']) + [
+    'src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml',
+  ],
+  deps = [
+    ':ui_module',
+    '//gerrit-common:client',
+    '//gerrit-extension-api:client',
+    '//lib:junit',
+    '//lib/gwt:dev',
+    '//lib/gwt:user',
+    '//lib/gwt:gwt-test-utils',
+    '//lib/jgit:jgit',
+  ],
+  source_under_test = [':ui_module'],
+  vm_args = ['-Xmx512m'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
new file mode 100644
index 0000000..e407854
--- /dev/null
+++ b/gerrit-gwtui/gwt.defs
@@ -0,0 +1,77 @@
+# 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.
+
+BROWSERS = [
+  'chrome',
+  'firefox',
+  'gecko1_8',
+  'safari',
+  'msie', 'ie6', 'ie8', 'ie9',
+]
+ALIASES = {
+  'chrome': 'safari',
+  'firefox': 'gecko1_8',
+  'msie': 'ie9',
+}
+MODULE = 'com.google.gerrit.GerritGwtUI'
+
+DEBUG_OPTS = [
+  '-style', 'PRETTY',
+  '-optimize', '0',
+]
+
+APP_DEPS = [':ui_module']
+
+def gwt_user_agent_permutations(
+    name,
+    module_name,
+    module_target,
+    compiler_opts = [],
+    deps = [],
+    browsers = [],
+    visibility = []):
+  for ua in browsers:
+    impl = ua
+    if ua in ALIASES:
+      impl = ALIASES[ua]
+    xml = ''.join([
+      "<module rename-to='%s'>" % module_name,
+      "<inherits name='%s'/>" % module_target,
+      "<set-property name='user.agent' value='%s'/>" % impl,
+      "<set-property name='locale' value='default'/>",
+      "</module>",
+    ])
+    gwt = '%s_%s.gwt.xml' % (module_target.replace('.', '/'), ua)
+    jar = '%s_%s.gwtxml.jar' % (name, ua)
+    genrule(
+      name = '%s_%s_gwtxml_gen' % (name, ua),
+      cmd = 'cd $TMP;' +
+        ('mkdir -p $(dirname %s);' % gwt) +
+        ('echo "%s">%s;' % (xml, gwt)) +
+        'zip -qr $OUT .',
+      out = jar,
+    )
+    prebuilt_jar(
+      name = '%s_%s_gwtxml_lib' % (name, ua),
+      binary_jar = genfile(jar),
+      deps = [':%s_%s_gwtxml_gen' % (name, ua)],
+    )
+    gwt_application(
+      name = '%s_%s' % (name, ua),
+      module_target = module_target + '_' + ua,
+      compiler_opts = compiler_opts,
+      deps = deps + [':%s_%s_gwtxml_lib' % (name, ua)],
+      visibility = visibility,
+    )
+
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
deleted file mode 100644
index 17065fe..0000000
--- a/gerrit-gwtui/pom.xml
+++ /dev/null
@@ -1,249 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-gwtui</artifactId>
-  <name>Gerrit Code Review - GWT UI</name>
-  <packaging>war</packaging>
-
-  <description>
-    Web interface built on top of Google Web Toolkit
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-user</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-gwtexpui</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-gwtexpui</artifactId>
-      <version>${project.version}</version>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <dependency>
-      <groupId>gwtjsonrpc</groupId>
-      <artifactId>gwtjsonrpc</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>gwtjsonrpc</groupId>
-      <artifactId>gwtjsonrpc</artifactId>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <dependency>
-      <groupId>gwtorm</groupId>
-      <artifactId>gwtorm</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>gwtorm</groupId>
-      <artifactId>gwtorm</artifactId>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-reviewdb</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-reviewdb</artifactId>
-      <version>${project.version}</version>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-common</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-common</artifactId>
-      <version>${project.version}</version>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-patch-jgit</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-patch-jgit</artifactId>
-      <version>${project.version}</version>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit</artifactId>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-prettify</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-prettify</artifactId>
-      <version>${project.version}</version>
-      <classifier>sources</classifier>
-      <type>jar</type>
-    </dependency>
-
-    <!-- GWT should require these itself, but doesn't. -->
-    <dependency>
-      <groupId>javax.validation</groupId>
-      <artifactId>validation-api</artifactId>
-      <scope>provided</scope>
-    </dependency>
-    <dependency>
-      <groupId>javax.validation</groupId>
-      <artifactId>validation-api</artifactId>
-      <classifier>sources</classifier>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>gwt-maven-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>optimized</id>
-            <configuration>
-              <module>com.google.gerrit.GerritGwtUI</module>
-              <extraJvmArgs>-Xmx512m</extraJvmArgs>
-              <compileReport>${gwt.compileReport}</compileReport>
-              <disableClassMetadata>true</disableClassMetadata>
-              <disableCastChecking>true</disableCastChecking>
-            </configuration>
-            <goals>
-              <goal>compile</goal>
-            </goals>
-          </execution>
-          <execution>
-            <id>debug</id>
-            <configuration>
-              <style>PRETTY</style>
-              <module>com.google.gerrit.GerritGwtUI</module>
-              <extraJvmArgs>-Xmx512m</extraJvmArgs>
-              <disableClassMetadata>true</disableClassMetadata>
-              <disableRunAsync>true</disableRunAsync>
-              <webappDirectory>${project.build.directory}/${project.build.finalName}_dbg</webappDirectory>
-            </configuration>
-            <goals>
-              <goal>compile</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-antrun-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>compress-html</id>
-            <phase>prepare-package</phase>
-            <goals>
-              <goal>run</goal>
-            </goals>
-            <configuration>
-              <target>
-                <property name="dst" location="${project.build.directory}/${project.build.finalName}"/>
-                <property name="dbg" location="${project.build.directory}/${project.build.finalName}_dbg"/>
-                <property name="app" location="${dst}/gerrit_ui"/>
-
-                <mkdir dir="${app}"/>
-                <apply executable="gzip" addsourcefile="false">
-                  <arg value="-9"/>
-                  <fileset dir="${app}">
-                    <include name="**/*.html"/>
-                    <include name="**/*.css"/>
-                    <include name="deferredjs/**/*.js"/>
-                  </fileset>
-                  <redirector>
-                    <inputmapper type="glob" from="*" to="${app}/*"/>
-                    <outputmapper type="glob" from="*" to="${app}/*.gz"/>
-                  </redirector>
-                </apply>
-
-                <copy file="${dbg}/gerrit_ui/gerrit_ui.nocache.js"
-                      tofile="${app}/gerrit_dbg.nocache.js"/>
-                <copy todir="${app}" overwrite="false">
-                  <fileset dir="${dbg}/gerrit_ui">
-                    <include name="**/*.html"/>
-                    <include name="**/*.css"/>
-                    <include name="deferredjs/**/*.js"/>
-                  </fileset>
-                </copy>
-              </target>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-war-plugin</artifactId>
-        <configuration>
-          <packagingExcludes>WEB-INF/classes/**,WEB-INF/deploy/**,WEB-INF/lib/**</packagingExcludes>
-          <attachClasses>true</attachClasses>
-          <archive>
-            <addMavenDescriptor>false</addMavenDescriptor>
-          </archive>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
index 5fdc5bb..9fe04bc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
@@ -24,15 +24,20 @@
   <inherits name='com.google.gwtexpui.linker.ServerPlannedIFrameLinker'/>
   <inherits name='com.google.gwtexpui.progress.Progress'/>
   <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
+  <inherits name='com.google.gerrit.extensions.Extensions'/>
   <inherits name='com.google.gerrit.prettify.PrettyFormatter'/>
   <inherits name='com.google.gerrit.Common'/>
   <inherits name='com.google.gerrit.UserAgent'/>
   <inherits name='org.eclipse.jgit.JGit'/>
+  <inherits name='net.codemirror.CodeMirror'/>
 
   <extend-property name='locale' values='en'/>
   <set-property-fallback name='locale' value='en'/>
   <set-property name='locale' value='en'/>
   <set-configuration-property name='UiBinder.useSafeHtmlTemplates' value='true'/>
 
+  <set-property name='gwt.logging.logLevel' value='SEVERE'/>
+  <set-property name='gwt.logging.popupHandler' value='DISABLED'/>
+
   <entry-point class='com.google.gerrit.client.Gerrit'/>
 </module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
index 1dce6df..9dde95c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
@@ -14,6 +14,13 @@
  limitations under the License.
 -->
 <module>
+  <replace-with class="com.google.gerrit.client.api.PluginName.PluginNameMoz">
+    <when-type-is class="com.google.gerrit.client.api.PluginName" />
+    <when-property-is name="compiler.stackMode" value="native" />
+    <when-property-is name="user.agent" value="safari" />
+    <when-property-is name="user.agent" value="gecko1_8" />
+  </replace-with>
+
   <replace-with class="com.google.gerrit.client.ui.FancyFlexTableImplIE6">
     <when-type-is class="com.google.gerrit.client.ui.FancyFlexTableImpl" />
     <any>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 5dcccb0..b8cc5c2d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client;
 
 import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.account.AccountInfo.AvatarInfo;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.event.dom.client.LoadEvent;
@@ -27,14 +28,15 @@
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.UIObject;
 
-public class AvatarImage extends Image {
-
+public class AvatarImage extends Image implements LoadHandler {
   public AvatarImage() {
+    setVisible(false);
+    addLoadHandler(this);
   }
 
   /** A default sized avatar image. */
   public AvatarImage(AccountInfo account) {
-    this(account, 0);
+    this(account, AccountInfo.AvatarInfo.DEFAULT_SIZE, true);
   }
 
   /**
@@ -60,26 +62,46 @@
    *        avatar image
    */
   public AvatarImage(AccountInfo account, int size, boolean addPopup) {
+    addLoadHandler(this);
     setAccount(account, size, addPopup);
   }
 
   public void setAccount(AccountInfo account, int size, boolean addPopup) {
-    setUrl(isGerritServer(account) ? getGerritServerAvatarUrl() :
-      url(account.email(), size));
-    setVisible(false);
-
-    if (size > 0) {
-      // If the provider does not resize the image, force it in the browser.
-      setSize(size + "px", size + "px");
-    }
-
-    addLoadHandler(new LoadHandler() {
-      @Override
-      public void onLoad(LoadEvent event) {
-        setVisible(true);
+    if (account == null) {
+      setVisible(false);
+    } else if (isGerritServer(account)) {
+      setVisible(true);
+      setResource(Gerrit.RESOURCES.gerritAvatar26());
+    } else if (account.has_avatar_info()) {
+      setVisible(false);
+      AvatarInfo info = account.avatar(size);
+      if (info != null) {
+        setWidth(info.width() > 0 ? info.width() + "px" : "");
+        setHeight(info.height() > 0 ? info.height() + "px" : "");
+        setUrl(info.url());
+        popup(account, addPopup);
       }
-    });
+    } else if (account.email() != null) {
+      // TODO Kill /accounts/*/avatar URL.
+      String u = account.email();
+      if (Gerrit.isSignedIn()
+          && u.equals(Gerrit.getUserAccount().getPreferredEmail())) {
+        u = "self";
+      }
+      RestApi api = new RestApi("/accounts/").id(u).view("avatar");
+      if (size > 0) {
+        api.addParameter("s", size);
+        setSize("", size + "px");
+      }
+      setVisible(false);
+      setUrl(api.url());
+      popup(account, addPopup);
+    } else {
+      setVisible(false);
+    }
+  }
 
+  private void popup(AccountInfo account, boolean addPopup) {
     if (addPopup) {
       PopupHandler popupHandler = new PopupHandler(account, this);
       addMouseOverHandler(popupHandler);
@@ -87,33 +109,17 @@
     }
   }
 
+  @Override
+  public void onLoad(LoadEvent event) {
+    setVisible(true);
+  }
+
   private static boolean isGerritServer(AccountInfo account) {
     return account._account_id() == 0
         && Util.C.messageNoAuthor().equals(account.name());
   }
 
-  private static String getGerritServerAvatarUrl() {
-    return Gerrit.RESOURCES.gerritAvatar().getSafeUri().asString();
-  }
-
-  private static String url(String email, int size) {
-    if (email == null) {
-      return "";
-    }
-    String u;
-    if (Gerrit.isSignedIn() && email.equals(Gerrit.getUserAccount().getPreferredEmail())) {
-      u = "self";
-    } else {
-      u = email;
-    }
-    RestApi api = new RestApi("/accounts/").id(u).view("avatar");
-    if (size > 0) {
-      api.addParameter("s", size);
-    }
-    return api.url();
-  }
-
-  private class PopupHandler implements MouseOverHandler, MouseOutHandler {
+  private static class PopupHandler implements MouseOverHandler, MouseOutHandler {
     private final AccountInfo account;
     private final UIObject target;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
index e3b7525..8fef111 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
@@ -18,12 +18,18 @@
  * Interface that a caller must implement to react on the result of a
  * {@link ConfirmationDialog}.
  */
-public interface ConfirmationCallback {
+public abstract class ConfirmationCallback {
 
   /**
    * Called when the {@link ConfirmationDialog} is finished with OK.
    * To be overwritten by subclasses.
    */
-  public void onOk();
+  public abstract void onOk();
 
+  /**
+   * Called when the {@link ConfirmationDialog} is finished with Cancel.
+   * To be overwritten by subclasses.
+   */
+  public void onCancel() {
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
index 36169db..e85633f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
@@ -16,7 +16,6 @@
 
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
@@ -49,12 +48,13 @@
     buttons.add(okButton);
 
     cancelButton = new Button();
-    DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px");
+    cancelButton.getElement().getStyle().setProperty("marginLeft", "300px");
     cancelButton.setText(Gerrit.C.confirmationDialogCancel());
     cancelButton.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
         hide();
+        callback.onCancel();
       }
     });
     buttons.add(cancelButton);
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 87a09cf..40e5252 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
@@ -60,6 +60,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.change.ChangeScreen2;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
 import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.client.changes.CustomDashboardScreen;
@@ -69,6 +70,7 @@
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.client.dashboards.DashboardInfo;
 import com.google.gerrit.client.dashboards.DashboardList;
+import com.google.gerrit.client.diff.SideBySide2;
 import com.google.gerrit.client.groups.GroupApi;
 import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.patches.PatchScreen;
@@ -78,6 +80,8 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -90,6 +94,8 @@
 import com.google.gwtorm.client.KeyUtil;
 
 public class Dispatcher {
+  public static boolean changeScreen2;
+
   public static String toPatchSideBySide(final Patch.Key id) {
     return toPatch("", null, id);
   }
@@ -98,6 +104,11 @@
     return toPatch("", diffBase, id);
   }
 
+  public static String toPatchSideBySide2(PatchSet.Id diffBase,
+      PatchSet.Id revision, String fileName) {
+    return toPatch("cm", diffBase, revision, fileName);
+  }
+
   public static String toPatchUnified(final Patch.Key id) {
     return toPatch("unified", null, id);
   }
@@ -107,14 +118,18 @@
   }
 
   private static String toPatch(String type, PatchSet.Id diffBase, Patch.Key id) {
-    PatchSet.Id ps = id.getParentKey();
-    Change.Id c = ps.getParentKey();
+    return toPatch(type, diffBase, id.getParentKey(), id.get());
+  }
+
+  private static String toPatch(String type, PatchSet.Id diffBase,
+      PatchSet.Id revision, String fileName) {
+    Change.Id c = revision.getParentKey();
     StringBuilder p = new StringBuilder();
     p.append("/c/").append(c).append("/");
     if (diffBase != null) {
       p.append(diffBase.get()).append("..");
     }
-    p.append(ps.get()).append("/").append(KeyUtil.encode(id.get()));
+    p.append(revision.get()).append("/").append(KeyUtil.encode(fileName));
     if (type != null && !type.isEmpty()) {
       p.append(",").append(type);
     }
@@ -210,6 +225,9 @@
     } else if (matchPrefix("/admin/", token)) {
       admin(token);
 
+    } else if (/* DEPRECATED URL */matchPrefix("/c2/", token)) {
+      changeScreen2 = true;
+      change(token);
     } else if (/* LEGACY URL */matchPrefix("all,", token)) {
       redirectFromLegacyToken(token, legacyAll(token));
     } else if (/* LEGACY URL */matchPrefix("mine,", token)
@@ -462,8 +480,10 @@
     }
 
     if (rest.isEmpty()) {
-      Gerrit.display(token, panel== null //
-          ? new ChangeScreen(id) //
+      Gerrit.display(token, panel== null
+          ? (isChangeScreen2()
+              ? new ChangeScreen2(id, null, false)
+              : new ChangeScreen(id))
           : new NotFoundScreen());
       return;
     }
@@ -494,7 +514,9 @@
       patch(token, base, p, 0, null, null, panel);
     } else {
       if (panel == null) {
-        Gerrit.display(token, new ChangeScreen(ps));
+        Gerrit.display(token, isChangeScreen2()
+            ? new ChangeScreen2(id, String.valueOf(ps.get()), false)
+            : new ChangeScreen(id));
       } else if ("publish".equals(panel)) {
         publish(ps);
       } else {
@@ -503,6 +525,23 @@
     }
   }
 
+  private static boolean isChangeScreen2() {
+    if (changeScreen2) {
+      return true;
+    }
+
+    AccountGeneralPreferences.ChangeScreen ui = null;
+    if (Gerrit.isSignedIn()) {
+      ui = Gerrit.getUserAccount()
+          .getGeneralPreferences()
+          .getChangeScreen();
+    }
+    if (ui == null) {
+      ui = Gerrit.getConfig().getChangeScreen();
+    }
+    return ui == AccountGeneralPreferences.ChangeScreen.CHANGE_SCREEN2;
+  }
+
   private static void publish(final PatchSet.Id ps) {
     String token = toPublish(ps);
     new AsyncSplit(token) {
@@ -567,6 +606,20 @@
                 top, //
                 baseId //
             );
+          } else if ("cm".equals(panel)) {
+            if (Gerrit.isSignedIn()
+                && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
+                    .getGeneralPreferences().getDiffView())) {
+              return new PatchScreen.Unified( //
+                  id, //
+                  patchIndex, //
+                  patchSetDetail, //
+                  patchTable, //
+                  top, //
+                  baseId //
+              );
+            }
+            return new SideBySide2(baseId, id.getParentKey(), id.get());
           }
         }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index 3adec8f..8048c4d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -21,7 +21,6 @@
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.http.client.Response;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.StatusCodeException;
 import com.google.gwt.user.client.ui.Button;
@@ -151,7 +150,7 @@
 
     if (msg != null) {
       final Label m = new Label(msg);
-      DOM.setStyleAttribute(m.getElement(), "whiteSpace", "pre");
+      m.getElement().getStyle().setProperty("whiteSpace", "pre");
       body.add(m);
     }
   }
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 0b1af89..568a15c 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
@@ -21,22 +21,28 @@
 import com.google.gerrit.client.account.AccountCapabilities;
 import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
+import com.google.gerrit.client.config.ConfigServerApi;
+import com.google.gerrit.client.extensions.TopMenu;
+import com.google.gerrit.client.extensions.TopMenuItem;
+import com.google.gerrit.client.extensions.TopMenuList;
 import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.LinkMenuBar;
 import com.google.gerrit.client.ui.LinkMenuItem;
 import com.google.gerrit.client.ui.MorphingTabPanel;
 import com.google.gerrit.client.ui.PatchLink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.client.ui.ScreenLoadEvent;
-import com.google.gerrit.common.ClientVersion;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.common.data.SystemInfoService;
+import com.google.gerrit.extensions.webui.GerritTopMenu;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
@@ -44,6 +50,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.Callback;
+import com.google.gwt.core.client.CodeDownloadException;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
@@ -73,7 +80,6 @@
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.InlineHTML;
 import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.user.client.UserAgent;
@@ -87,6 +93,9 @@
 import com.google.gwtorm.client.KeyUtil;
 
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 public class Gerrit implements EntryPoint {
   public static final GerritConstants C = GWT.create(GerritConstants.class);
@@ -105,17 +114,20 @@
   private static AccountDiffPreference myAccountDiffPref;
   private static String xGerritAuth;
 
+  private static Map<String, LinkMenuBar> menuBars;
+
   private static MorphingTabPanel menuLeft;
   private static LinkMenuBar menuRight;
-  private static LinkMenuBar diffBar;
-  private static LinkMenuBar projectsBar;
+  private static RootPanel topMenu;
   private static RootPanel siteHeader;
   private static RootPanel siteFooter;
+  private static RootPanel bottomMenu;
   private static SearchPanel searchPanel;
   private static final Dispatcher dispatcher = new Dispatcher();
   private static ViewSite<Screen> body;
   private static PatchScreen patchScreen;
   private static String lastChangeListToken;
+  private static String lastViewToken;
 
   static {
     SYSTEM_SVC = GWT.create(SystemInfoService.class);
@@ -144,6 +156,10 @@
     }
   }
 
+  public static String getPriorView() {
+    return lastViewToken;
+  }
+
   /**
    * Load the screen at the given location, displaying when ready.
    * <p>
@@ -188,6 +204,7 @@
    * @param view the loaded view.
    */
   public static void updateMenus(Screen view) {
+    LinkMenuBar diffBar = menuBars.get(GerritTopMenu.DIFFERENCES.menuName);
     if (view instanceof PatchScreen) {
       patchScreen = (PatchScreen) view;
       menuLeft.setVisible(diffBar, true);
@@ -230,6 +247,35 @@
     }
   }
 
+  public static int getHeaderFooterHeight() {
+    int h = bottomMenu.getOffsetHeight();
+    if (topMenu.isVisible()) {
+      h += topMenu.getOffsetHeight();
+    }
+    if (siteHeader.isVisible()) {
+      h += siteHeader.getOffsetHeight();
+    }
+    if (siteFooter.isVisible()) {
+      h += siteFooter.getOffsetHeight();
+    }
+    return h;
+  }
+
+  public static void setHeaderVisible(boolean visible) {
+    topMenu.setVisible(visible);
+    siteHeader.setVisible(visible && (myAccount != null
+        ? myAccount.getGeneralPreferences().isShowSiteHeader()
+        : true));
+  }
+
+  public static boolean isHeaderVisible() {
+    return topMenu.isVisible();
+  }
+
+  public static RootPanel getBottomMenu() {
+    return bottomMenu;
+  }
+
   /** Get the public configuration data used by this Gerrit instance. */
   public static GerritConfig getConfig() {
     return myConfig;
@@ -434,47 +480,36 @@
     }
   }
 
-  private static void populateBottomMenu(final RootPanel btmmenu) {
-    final Label keyHelp = new Label(C.keyHelp());
-    keyHelp.setStyleName(RESOURCES.css().keyhelp());
-    btmmenu.add(keyHelp);
-
-    String vs;
-    if (GWT.isScript()) {
-      final ClientVersion v = GWT.create(ClientVersion.class);
-      vs = v.version().getText();
-      if (vs.startsWith("v")) {
-        vs = vs.substring(1);
-      }
-    } else {
+  private static void populateBottomMenu(RootPanel btmmenu, HostPageData hpd) {
+    String vs = hpd.version;
+    if (vs == null || vs.isEmpty()) {
       vs = "dev";
     }
 
-    FlowPanel poweredBy = new FlowPanel();
-    poweredBy.setStyleName(RESOURCES.css().version());
-    poweredBy.add(new InlineHTML(M.poweredBy(vs)));
+    btmmenu.add(new InlineLabel(C.keyHelp()));
+    btmmenu.add(new InlineLabel(" | "));
+    btmmenu.add(new InlineHTML(M.poweredBy(vs)));
     if (getConfig().getReportBugUrl() != null) {
-      poweredBy.add(new InlineLabel(" | "));
       Anchor a = new Anchor(
           C.reportBug(),
           getConfig().getReportBugUrl());
       a.setTarget("_blank");
       a.setStyleName("");
-      poweredBy.add(a);
+      btmmenu.add(new InlineLabel(" | "));
+      btmmenu.add(a);
     }
-    btmmenu.add(poweredBy);
   }
 
   private void onModuleLoad2(HostPageData hpd) {
     RESOURCES.gwt_override().ensureInjected();
     RESOURCES.css().ensureInjected();
 
-    final RootPanel gTopMenu = RootPanel.get("gerrit_topmenu");
+    topMenu = RootPanel.get("gerrit_topmenu");
     final RootPanel gStarting = RootPanel.get("gerrit_startinggerrit");
     final RootPanel gBody = RootPanel.get("gerrit_body");
-    final RootPanel gBottomMenu = RootPanel.get("gerrit_btmmenu");
+    bottomMenu = RootPanel.get("gerrit_btmmenu");
 
-    gTopMenu.setStyleName(RESOURCES.css().gerritTopMenu());
+    topMenu.setStyleName(RESOURCES.css().gerritTopMenu());
     gBody.setStyleName(RESOURCES.css().gerritBody());
 
     final Grid menuLine = new Grid(1, 3);
@@ -483,7 +518,7 @@
     searchPanel = new SearchPanel();
     menuLeft.setStyleName(RESOURCES.css().topmenuMenuLeft());
     menuLine.setStyleName(RESOURCES.css().topmenu());
-    gTopMenu.add(menuLine);
+    topMenu.add(menuLine);
     final FlowPanel menuRightPanel = new FlowPanel();
     menuRightPanel.setStyleName(RESOURCES.css().topmenuMenuRight());
     menuRightPanel.add(searchPanel);
@@ -502,8 +537,9 @@
     body = new ViewSite<Screen>() {
       @Override
       protected void onShowView(Screen view) {
-        final String token = view.getToken();
-        if (!token.equals(History.getToken())) {
+        lastViewToken = History.getToken();
+        String token = view.getToken();
+        if (!token.equals(lastViewToken)) {
           History.newItem(token, false);
           dispatchHistoryHooks(token);
         }
@@ -518,7 +554,7 @@
     };
     gBody.add(body);
 
-    RpcStatus.INSTANCE = new RpcStatus(gTopMenu);
+    RpcStatus.INSTANCE = new RpcStatus();
     JsonUtil.addRpcStartHandler(RpcStatus.INSTANCE);
     JsonUtil.addRpcCompleteHandler(RpcStatus.INSTANCE);
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
@@ -539,7 +575,7 @@
 
     applyUserPreferences();
     initHistoryHooks();
-    populateBottomMenu(gBottomMenu);
+    populateBottomMenu(bottomMenu, hpd);
     refreshMenuBar();
 
     History.addValueChangeHandler(new ValueChangeHandler<String>() {
@@ -571,7 +607,8 @@
   }
 
   private void loadPlugins(HostPageData hpd, final String token) {
-    if (hpd.plugins != null) {
+    ApiGlue.init();
+    if (hpd.plugins != null && !hpd.plugins.isEmpty()) {
       for (final String url : hpd.plugins) {
         ScriptInjector.fromUrl(url)
             .setWindow(ScriptInjector.TOP_WINDOW)
@@ -582,8 +619,12 @@
 
               @Override
               public void onFailure(Exception reason) {
-                ErrorDialog d = new ErrorDialog(reason);
-                d.setTitle(M.pluginFailed(url));
+                ErrorDialog d;
+                if (reason instanceof CodeDownloadException) {
+                  d = new ErrorDialog(M.cannotDownloadPlugin(url));
+                } else {
+                  d = new ErrorDialog(M.pluginFailed(url));
+                }
                 d.center();
               }
             }).inject();
@@ -617,11 +658,14 @@
     menuLeft.clear();
     menuRight.clear();
 
+    menuBars = new HashMap<String, LinkMenuBar>();
+
     final boolean signedIn = isSignedIn();
     final GerritConfig cfg = getConfig();
     LinkMenuBar m;
 
     m = new LinkMenuBar();
+    menuBars.put(GerritTopMenu.ALL.menuName, m);
     addLink(m, C.menuAllOpen(), PageLinks.toChangeQuery("status:open"));
     addLink(m, C.menuAllMerged(), PageLinks.toChangeQuery("status:merged"));
     addLink(m, C.menuAllAbandoned(), PageLinks.toChangeQuery("status:abandoned"));
@@ -629,6 +673,7 @@
 
     if (signedIn) {
       m = new LinkMenuBar();
+      menuBars.put(GerritTopMenu.MY.menuName, m);
       addLink(m, C.menuMyChanges(), PageLinks.MINE);
       addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
       addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
@@ -641,7 +686,8 @@
     }
 
     patchScreen = null;
-    diffBar = new LinkMenuBar();
+    LinkMenuBar diffBar = new LinkMenuBar();
+    menuBars.put(GerritTopMenu.DIFFERENCES.menuName, diffBar);
     menuLeft.addInvisible(diffBar, C.menuDiff());
     addDiffLink(diffBar, CC.patchTableDiffSideBySide(), PatchScreen.Type.SIDE_BY_SIDE);
     addDiffLink(diffBar, CC.patchTableDiffUnified(), PatchScreen.Type.UNIFIED);
@@ -650,7 +696,7 @@
     addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
     addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
 
-    projectsBar = new LinkMenuBar() {
+    final LinkMenuBar projectsBar = new LinkMenuBar() {
       @Override
       public void onScreenLoad(ScreenLoadEvent event) {
         if (event.getScreen() instanceof ProjectScreen) {
@@ -658,30 +704,41 @@
         }
       }
     };
+    menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
     addProjectLink(projectsBar, C.menuProjectsInfo(), ProjectScreen.INFO);
     addProjectLink(projectsBar, C.menuProjectsBranches(), ProjectScreen.BRANCH);
     addProjectLink(projectsBar, C.menuProjectsAccess(), ProjectScreen.ACCESS);
-    addProjectLink(projectsBar, C.menuProjectsDashboards(), ProjectScreen.DASHBOARDS);
+    final LinkMenuItem dashboardsMenuItem =
+        addProjectLink(projectsBar, C.menuProjectsDashboards(),
+            ProjectScreen.DASHBOARDS);
     menuLeft.add(projectsBar, C.menuProjects());
 
     if (signedIn) {
       final LinkMenuBar peopleBar = new LinkMenuBar();
-      addLink(peopleBar, C.menuPeopleGroupsList(), PageLinks.ADMIN_GROUPS);
+      menuBars.put(GerritTopMenu.PEOPLE.menuName, peopleBar);
+      final LinkMenuItem groupsListMenuItem =
+          addLink(peopleBar, C.menuPeopleGroupsList(), PageLinks.ADMIN_GROUPS);
       menuLeft.add(peopleBar, C.menuPeople());
 
       final LinkMenuBar pluginsBar = new LinkMenuBar();
+      menuBars.put(GerritTopMenu.PLUGINS.menuName, pluginsBar);
       AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
         @Override
         public void onSuccess(AccountCapabilities result) {
           if (result.canPerform(CREATE_PROJECT)) {
-            addLink(projectsBar, C.menuProjectsCreate(), PageLinks.ADMIN_CREATE_PROJECT);
+            insertLink(projectsBar, C.menuProjectsCreate(),
+                PageLinks.ADMIN_CREATE_PROJECT,
+                projectsBar.getWidgetIndex(dashboardsMenuItem) + 1);
           }
           if (result.canPerform(CREATE_GROUP)) {
-            addLink(peopleBar, C.menuPeopleGroupsCreate(), PageLinks.ADMIN_CREATE_GROUP);
+            insertLink(peopleBar, C.menuPeopleGroupsCreate(),
+                PageLinks.ADMIN_CREATE_GROUP,
+                peopleBar.getWidgetIndex(groupsListMenuItem) + 1);
           }
           if (result.canPerform(ADMINISTRATE_SERVER)) {
-            addLink(pluginsBar, C.menuPluginsInstalled(), PageLinks.ADMIN_PLUGINS);
+            insertLink(pluginsBar, C.menuPluginsInstalled(),
+                PageLinks.ADMIN_PLUGINS, 0);
             menuLeft.insert(pluginsBar, C.menuPlugins(),
                 menuLeft.getWidgetIndex(peopleBar) + 1);
           }
@@ -691,6 +748,7 @@
 
     if (getConfig().isDocumentationAvailable()) {
       m = new LinkMenuBar();
+      menuBars.put(GerritTopMenu.DOCUMENTATION.menuName, m);
       addDocLink(m, C.menuDocumentationIndex(), "index.html");
       addDocLink(m, C.menuDocumentationSearch(), "user-search.html");
       addDocLink(m, C.menuDocumentationUpload(), "user-upload.html");
@@ -703,8 +761,6 @@
       whoAmI(cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP);
     } else {
       switch (cfg.getAuthType()) {
-        case HTTP:
-        case HTTP_LDAP:
         case CLIENT_SSL_CERT_LDAP:
           break;
 
@@ -733,6 +789,14 @@
           });
           break;
 
+        case HTTP:
+        case HTTP_LDAP:
+          if (cfg.getLoginUrl() != null) {
+            final String signinText = cfg.getLoginText() == null ? C.menuSignIn() : cfg.getLoginText();
+            menuRight.add(anchor(signinText, cfg.getLoginUrl()));
+          }
+          break;
+
         case LDAP:
         case LDAP_BIND:
         case CUSTOM_EXTENSION:
@@ -752,6 +816,22 @@
           break;
       }
     }
+    ConfigServerApi.topMenus(new GerritCallback<TopMenuList>() {
+      public void onSuccess(TopMenuList result) {
+        List<TopMenu> topMenuExtensions = Natives.asList(result);
+        for (TopMenu menu : topMenuExtensions) {
+          LinkMenuBar existingBar = menuBars.get(menu.getName());
+          LinkMenuBar bar = existingBar != null ? existingBar : new LinkMenuBar();
+          for (TopMenuItem item : Natives.asList(menu.getItems())) {
+            addExtensionLink(bar, item);
+          }
+          if (existingBar == null) {
+            menuBars.put(menu.getName(), bar);
+            menuLeft.add(bar, menu.getName());
+          }
+        }
+      }
+    });
   }
 
   public static void applyUserPreferences() {
@@ -803,7 +883,8 @@
     userSummaryPanel.setStyleName(RESOURCES.css().menuBarUserNamePanel());
     userSummaryPanel.add(l);
     userSummaryPanel.add(avatar);
-    userSummaryPanel.add(new InlineLabel(" â–¾"));
+    // "BLACK DOWN-POINTING SMALL TRIANGLE"
+    userSummaryPanel.add(new InlineLabel(" \u25be"));
     userPopup.addAutoHidePartner(userSummaryPanel.getElement());
     FocusPanel fp = new FocusPanel(userSummaryPanel);
     fp.setStyleName(RESOURCES.css().menuBarUserNameFocusPanel());
@@ -819,9 +900,16 @@
     return a;
   }
 
-  private static void addLink(final LinkMenuBar m, final String text,
+  private static LinkMenuItem addLink(final LinkMenuBar m, final String text,
       final String historyToken) {
-    m.addItem(new LinkMenuItem(text, historyToken));
+    LinkMenuItem i = new LinkMenuItem(text, historyToken);
+    m.addItem(i);
+    return i;
+  }
+
+  private static void insertLink(final LinkMenuBar m, final String text,
+      final String historyToken, final int beforeIndex) {
+    m.insertItem(new LinkMenuItem(text, historyToken), beforeIndex);
   }
 
   private static void addDiffLink(final LinkMenuBar m, final String text,
@@ -837,9 +925,9 @@
       });
   }
 
-  private static void addProjectLink(final LinkMenuBar m, final String text,
+  private static LinkMenuItem addProjectLink(final LinkMenuBar m, final String text,
       final String panel) {
-    m.addItem(new LinkMenuItem(text, "") {
+    LinkMenuItem i = new LinkMenuItem(text, "") {
         @Override
         public void onScreenLoad(ScreenLoadEvent event) {
           Screen screen = event.getScreen();
@@ -858,7 +946,9 @@
           }
           super.onScreenLoad(event);
         }
-      });
+      };
+    m.addItem(i);
+    return i;
   }
 
   private static void addDiffLink(final LinkMenuBar m, final String text,
@@ -884,4 +974,13 @@
     atag.setTarget("_blank");
     m.add(atag);
   }
+
+  private static void addExtensionLink(final LinkMenuBar m, final TopMenuItem item) {
+    final Anchor atag = anchor(item.getName(), item.getUrl());
+    atag.setTarget(item.getTarget());
+    if (item.getId() != null) {
+      atag.getElement().setAttribute("id", item.getId());
+    }
+    m.add(atag);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 683f058..717bf8c 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
@@ -18,9 +18,7 @@
 
 public interface GerritConstants extends Constants {
   String menuSignIn();
-  String menuSignOut();
   String menuRegister();
-  String menuSettings();
   String reportBug();
 
   String signInDialogTitle();
@@ -50,6 +48,8 @@
 
   String inactiveAccountBody();
 
+  String labelNotApplicable();
+
   String menuAll();
   String menuAllOpen();
   String menuAllMerged();
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 defc7e4..4c4fcf7 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
@@ -1,7 +1,5 @@
 menuSignIn = Sign In
-menuSignOut = Sign Out
 menuRegister = Register
-menuSettings = Settings
 reportBug = Report Bug
 
 signInDialogTitle = Code Review - Sign In
@@ -33,6 +31,8 @@
 
 inactiveAccountBody = This user is currently inactive.
 
+labelNotApplicable = Label not applicable
+
 menuAll = All
 menuAllOpen = Open
 menuAllMerged = Merged
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 a33556e..9842049 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
@@ -92,6 +92,12 @@
   String diffTextHunkHeader();
   String diffTextINSERT();
   String diffTextNoLF();
+  String downloadBox();
+  String downloadBoxTable();
+  String downloadBoxTableCommandColumn();
+  String downloadBoxSpacer();
+  String downloadBoxScheme();
+  String downloadBoxCopyLabel();
   String downloadLink();
   String downloadLinkCopyLabel();
   String downloadLinkHeader();
@@ -140,14 +146,15 @@
   String infoBlock();
   String infoTable();
   String inputFieldTypeHint();
-  String keyhelp();
   String labelList();
+  String labelNotApplicable();
   String leftMostCell();
   String lineHeader();
   String lineNumber();
   String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
+  String maxObjectSizeLimitPanel();
   String menuBarUserName();
   String menuBarUserNameAvatar();
   String menuBarUserNameFocusPanel();
@@ -181,10 +188,12 @@
   String patchSizeCell();
   String pluginsTable();
   String posscore();
+  String projectActions();
   String projectAdminLabelRangeLine();
   String projectAdminLabelValue();
   String projectFilterLabel();
   String projectFilterPanel();
+  String projectNameColumn();
   String publishCommentsScreen();
   String registerScreenExplain();
   String registerScreenNextLinks();
@@ -195,12 +204,11 @@
   String rightBorder();
   String rightmost();
   String rpcStatus();
-  String rpcStatusLoading();
-  String rpcStatusPanel();
   String screen();
   String screenHeader();
   String screenNoHeader();
   String searchPanel();
+  String suggestBoxPopup();
   String sectionHeader();
   String selectPatchSetOldVersion();
   String sideBySideScreenLinkTable();
@@ -230,6 +238,5 @@
   String userInfoPopup();
   String useridentity();
   String usernameField();
-  String version();
   String watchedProjectFilter();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
index 8fc196a..cbb5513 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
@@ -33,4 +33,5 @@
   String branchCreationConflict(String branchName, String existingBranchName);
 
   String pluginFailed(String scriptPath);
+  String cannotDownloadPlugin(String scriptPath);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index 41caa44..5ab8b3b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -14,3 +14,4 @@
 branchCreationConflict = Cannot create branch {0} since it conflicts with branch {1}.
 
 pluginFailed = Plugin JavaScript {0} failed to load
+cannotDownloadPlugin = Cannot download JavaScript plugin from: {0}.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index 098cc77..80d65b0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -52,8 +52,8 @@
   @Source("addFileComment.png")
   public ImageResource addFileComment();
 
-  @Source("diffy.png")
-  public ImageResource gerritAvatar();
+  @Source("diffy26.png")
+  public ImageResource gerritAvatar26();
 
   @Source("draftComments.png")
   public ImageResource draftComments();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
index ec97e58..216395b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.common.data.GitWebType;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -45,19 +46,26 @@
     return !ps.isDraft() || type.getLinkDrafts();
   }
 
+  public boolean canLink(RevisionInfo revision) {
+    return revision.draft() || type.getLinkDrafts();
+  }
+
   public String getLinkName() {
     return "(" + type.getLinkName() + ")";
   }
 
-  public String toRevision(final Project.NameKey project, final PatchSet ps) {
+  public String toRevision(String  project, String commit) {
     ParameterizedString pattern = new ParameterizedString(type.getRevision());
-
-    final Map<String, String> p = new HashMap<String, String>();
-    p.put("project", encode(project.get()));
-    p.put("commit", encode(ps.getRevision().get()));
+    Map<String, String> p = new HashMap<String, String>();
+    p.put("project", encode(project));
+    p.put("commit", encode(commit));
     return baseUrl + pattern.replace(p);
   }
 
+  public String toRevision(final Project.NameKey project, final PatchSet ps) {
+    return toRevision(project.get(), ps.getRevision().get());
+  }
+
   public String toProject(final Project.NameKey project) {
     ParameterizedString pattern = new ParameterizedString(type.getProject());
 
@@ -86,6 +94,10 @@
   }
 
   private String encode(String segment) {
-    return URL.encodeQueryString(type.replacePathSeparator(segment));
+    if (type.isUrlEncode()) {
+      return URL.encodeQueryString(type.replacePathSeparator(segment));
+    } else {
+      return segment;
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
index f354496..6a417ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
@@ -18,7 +18,6 @@
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -52,7 +51,7 @@
     buttons.add(signin);
 
     final Button close = new Button();
-    DOM.setStyleAttribute(close.getElement(), "marginLeft", "200px");
+    close.getElement().getStyle().setProperty("marginLeft", "200px");
     close.setText(Gerrit.C.signInDialogClose());
     close.addClickHandler(new ClickHandler() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
index 3298a06..7b6009a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -23,19 +23,19 @@
  * in the format defined by {@code git log --relative-date}.
  */
 public class RelativeDateFormatter {
-  final static long SECOND_IN_MILLIS = 1000;
+  static final long SECOND_IN_MILLIS = 1000;
 
-  final static long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
+  static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
 
-  final static long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
+  static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
 
-  final static long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
+  static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
 
-  final static long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
+  static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
 
-  final static long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
+  static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
 
-  final static long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
+  static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
 
   /**
    * @param when {@link Date} to format
@@ -51,32 +51,62 @@
 
     // seconds
     if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) {
-      return Util.M.secondsAgo(round(ageMillis, SECOND_IN_MILLIS));
+      long seconds = round(ageMillis, SECOND_IN_MILLIS);
+      if (seconds == 1) {
+        return Util.C.oneSecondAgo();
+      } else {
+        return Util.M.secondsAgo(seconds);
+      }
     }
 
     // minutes
     if (ageMillis < upperLimit(HOUR_IN_MILLIS)) {
-      return Util.M.minutesAgo(round(ageMillis, MINUTE_IN_MILLIS));
+      long minutes = round(ageMillis, MINUTE_IN_MILLIS);
+      if (minutes == 1) {
+        return Util.C.oneMinuteAgo();
+      } else {
+        return Util.M.minutesAgo(minutes);
+      }
     }
 
     // hours
     if (ageMillis < upperLimit(DAY_IN_MILLIS)) {
-      return Util.M.hoursAgo(round(ageMillis, HOUR_IN_MILLIS));
+      long hours = round(ageMillis, HOUR_IN_MILLIS);
+      if (hours == 1) {
+        return Util.C.oneHourAgo();
+      } else {
+        return Util.M.hoursAgo(hours);
+      }
     }
 
     // up to 14 days use days
     if (ageMillis < 14 * DAY_IN_MILLIS) {
-      return Util.M.daysAgo(round(ageMillis, DAY_IN_MILLIS));
+      long days = round(ageMillis, DAY_IN_MILLIS);
+      if (days == 1) {
+        return Util.C.oneDayAgo();
+      } else {
+        return Util.M.daysAgo(days);
+      }
     }
 
     // up to 10 weeks use weeks
     if (ageMillis < 10 * WEEK_IN_MILLIS) {
-      return Util.M.weeksAgo(round(ageMillis, WEEK_IN_MILLIS));
+      long weeks = round(ageMillis, WEEK_IN_MILLIS);
+      if (weeks == 1) {
+        return Util.C.oneWeekAgo();
+      } else {
+        return Util.M.weeksAgo(weeks);
+      }
     }
 
     // months
     if (ageMillis < YEAR_IN_MILLIS) {
-      return Util.M.monthsAgo(round(ageMillis, MONTH_IN_MILLIS));
+      long months = round(ageMillis, MONTH_IN_MILLIS);
+      if (months == 1) {
+        return Util.C.oneMonthAgo();
+      } else {
+        return Util.M.monthsAgo(months);
+      }
     }
 
     // up to 5 years use "year, months" rounded to months
@@ -94,7 +124,12 @@
     }
 
     // years
-    return Util.M.yearsAgo(round(ageMillis, YEAR_IN_MILLIS));
+    long years = round(ageMillis, YEAR_IN_MILLIS);
+    if (years == 1) {
+      return Util.C.oneYearAgo();
+    } else {
+      return Util.M.yearsAgo(years);
+    }
   }
 
   private static long upperLimit(long unit) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
index 955c8e2..cd715c6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.client;
 
-import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwtjsonrpc.client.event.RpcCompleteEvent;
 import com.google.gwtjsonrpc.client.event.RpcCompleteHandler;
 import com.google.gwtjsonrpc.client.event.RpcStartEvent;
@@ -41,17 +40,12 @@
   private final Label loading;
   private int activeCalls;
 
-  RpcStatus(final Panel p) {
-    final FlowPanel r = new FlowPanel();
-    r.setStyleName(Gerrit.RESOURCES.css().rpcStatusPanel());
-    p.add(r);
-
+  RpcStatus() {
     loading = new InlineLabel();
     loading.setText(Gerrit.C.rpcStatusWorking());
     loading.setStyleName(Gerrit.RESOURCES.css().rpcStatus());
-    loading.addStyleName(Gerrit.RESOURCES.css().rpcStatusLoading());
     loading.setVisible(false);
-    r.add(loading);
+    RootPanel.get().add(loading);
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index 46b7d4d..f86e1fe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -119,6 +119,12 @@
   private static class MySuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
     private boolean isSuggestionSelected;
 
+    private MySuggestionDisplay() {
+      super();
+
+      getPopupPanel().addStyleName(Gerrit.RESOURCES.css().suggestBoxPopup());
+    }
+
     @Override
     protected Suggestion getCurrentSelection() {
       Suggestion currentSelection = super.getCurrentSelection();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index dd4de33..da9f1c6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -79,6 +79,7 @@
     suggestions.add("reviewerin:");
 
     suggestions.add("commit:");
+    suggestions.add("comment:");
     suggestions.add("project:");
     suggestions.add("branch:");
     suggestions.add("topic:");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
index a532209..ceb50a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.client;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 01811a6..90348db 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -15,42 +15,34 @@
 package com.google.gerrit.client;
 
 import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 
 public class UserPopupPanel extends PluginSafePopupPanel {
-  interface Binder extends UiBinder<Widget, UserPopupPanel> {
-  }
-
+  interface Binder extends UiBinder<Widget, UserPopupPanel> {}
   private static final Binder binder = GWT.create(Binder.class);
 
-  @UiField(provided = true)
-  AvatarImage avatar;
-  @UiField
-  Label userName;
-  @UiField
-  Label userEmail;
-  @UiField
-  Anchor logout;
-  @UiField
-  Anchor settings;
+  @UiField(provided = true) AvatarImage avatar;
+  @UiField Label userName;
+  @UiField Label userEmail;
+  @UiField Element userLinks;
+  @UiField AnchorElement switchAccount;
+  @UiField AnchorElement logout;
+  @UiField InlineHyperlink settings;
 
   public UserPopupPanel(AccountInfo account, boolean canLogOut,
       boolean showSettingsLink) {
     super(/* auto hide */true, /* modal */false);
     avatar = new AvatarImage(account, 100, false);
     setWidget(binder.createAndBindUi(this));
-    // We must show and then hide this popup so that it is part of the DOM.
-    // Otherwise the image does not get any events.  Calling hide() would
-    // remove it from the DOM so we use setVisible(false) instead.
-    show();
-    setVisible(false);
     setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
     if (account.name() != null) {
       userName.setText(account.name());
@@ -58,15 +50,35 @@
     if (account.email() != null) {
       userEmail.setText(account.email());
     }
-    if (canLogOut) {
-      logout.setHref(Gerrit.selfRedirect("/logout"));
-    } else {
-      logout.setVisible(false);
-    }
     if (showSettingsLink) {
-      settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+      if (Gerrit.getConfig().getSwitchAccountUrl() != null) {
+        switchAccount.setHref(Gerrit.getConfig().getSwitchAccountUrl());
+      } else if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT
+          || Gerrit.getConfig().getAuthType() == AuthType.OPENID) {
+        switchAccount.setHref(Gerrit.selfRedirect("/login/"));
+      } else {
+        switchAccount.removeFromParent();
+        switchAccount = null;
+      }
+      if (canLogOut) {
+        logout.setHref(Gerrit.selfRedirect("/logout"));
+      } else {
+        logout.removeFromParent();
+        logout = null;
+      }
+
     } else {
-      settings.setVisible(false);
+      settings.removeFromParent();
+      settings = null;
+      userLinks.removeFromParent();
+      userLinks = null;
+      logout = null;
     }
+
+    // We must show and then hide this popup so that it is part of the DOM.
+    // Otherwise the image does not get any events.  Calling hide() would
+    // remove it from the DOM so we use setVisible(false) instead.
+    show();
+    setVisible(false);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
index 0db5788..cd51485 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
@@ -17,9 +17,8 @@
 <!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
   xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:gerrit='urn:import:com.google.gerrit.client'>
-  <ui:with field='constants' type='com.google.gerrit.client.GerritConstants'/>
-
+  xmlns:gerrit='urn:import:com.google.gerrit.client'
+  xmlns:u='urn:import:com.google.gerrit.client.ui'>
   <ui:style>
     .panel {
       padding: 8px;
@@ -38,10 +37,17 @@
     .email {
       padding-bottom: 6px;
     }
-    .logout {
-      padding-left: 16px;
+    .userLinks {
+      min-width: 250px;
+    }
+    .userLinksRight {
       float: right;
     }
+    .switchAccount {
+      border-right: 1px solid black;
+      padding-right: 0.5em;
+      margin-right: 0.5em;
+    }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.panel}'>
@@ -51,11 +57,14 @@
       <g:Label ui:field='userName' styleName="{style.userName}" />
       <g:Label ui:field='userEmail' styleName="{style.email}" />
     </td></tr></table>
-    <g:Anchor ui:field='settings'>
-      <ui:text from='{constants.menuSettings}' />
-    </g:Anchor>
-    <g:Anchor ui:field='logout' styleName="{style.logout}">
-      <ui:text from='{constants.menuSignOut}' />
-    </g:Anchor>
+    <div ui:field='userLinks' class='{style.userLinks}'>
+      <u:InlineHyperlink ui:field='settings' targetHistoryToken='/settings/'>
+        <ui:msg>Settings</ui:msg>
+      </u:InlineHyperlink>
+      <span class='{style.userLinksRight}'>
+        <a ui:field='switchAccount' class='{style.switchAccount}'><ui:msg>Switch Account</ui:msg></a
+        ><a ui:field='logout'><ui:msg>Sign Out</ui:msg></a>
+      </span>
+    </div>
   </g:HTMLPanel>
-</ui:UiBinder>
\ No newline at end of file
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java
new file mode 100644
index 0000000..629f725
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.access;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.Collections;
+import java.util.Set;
+
+/** Access rights available from {@code /access/}. */
+public class AccessMap extends NativeMap<ProjectAccessInfo> {
+  public static void get(Set<Project.NameKey> projects,
+      AsyncCallback<AccessMap> callback) {
+    RestApi api = new RestApi("/access/");
+    for (Project.NameKey p : projects) {
+      api.addParameter("project", p.get());
+    }
+    api.get(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void get(final Project.NameKey project,
+      final AsyncCallback<ProjectAccessInfo> cb) {
+    get(Collections.singleton(project), new AsyncCallback<AccessMap>() {
+      @Override
+      public void onSuccess(AccessMap result) {
+        cb.onSuccess(result.get(project.get()));
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        cb.onFailure(caught);
+      }
+    });
+  }
+
+  protected AccessMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
new file mode 100644
index 0000000..7cfb1fc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.access;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ProjectAccessInfo extends JavaScriptObject {
+  public final native boolean canAddRefs() /*-{ return this.can_add ? true : false; }-*/;
+  public final native boolean isOwner() /*-{ return this.is_owner ? true : false; }-*/;
+
+  protected ProjectAccessInfo() {
+  }
+}
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
new file mode 100644
index 0000000..06fad36
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.Set;
+
+/**
+ * A collection of static methods which work on the Gerrit REST API for specific
+ * accounts.
+ */
+public class AccountApi {
+  public static RestApi self() {
+    return new RestApi("/accounts/").view("self");
+  }
+
+  /** Retrieve the username */
+  public static void getUsername(String account, AsyncCallback<NativeString> cb) {
+    new RestApi("/accounts/").id(account).view("username").get(cb);
+  }
+
+  /** Retrieve email addresses */
+  public static void getEmails(String account,
+      AsyncCallback<JsArray<EmailInfo>> cb) {
+    new RestApi("/accounts/").id(account).view("emails").get(cb);
+  }
+
+  /** Register a new email address */
+  public static void registerEmail(String account, String email,
+      AsyncCallback<EmailInfo> cb) {
+    JavaScriptObject in = JavaScriptObject.createObject();
+    new RestApi("/accounts/").id(account).view("emails").id(email)
+        .ifNoneMatch().put(in, cb);
+  }
+
+  /** Retrieve SSH keys */
+  public static void getSshKeys(String account,
+      AsyncCallback<JsArray<SshKeyInfo>> cb) {
+    new RestApi("/accounts/").id(account).view("sshkeys").get(cb);
+  }
+
+  /** Add a new SSH keys */
+  public static void addSshKey(String account, String sshPublicKey,
+      AsyncCallback<SshKeyInfo> cb) {
+    new RestApi("/accounts/").id(account).view("sshkeys")
+        .post(sshPublicKey, cb);
+  }
+
+  /**
+   * Delete SSH keys. For each key to be deleted a separate DELETE request is
+   * fired to the server. The {@code onSuccess} method of the provided callback
+   * is invoked once after all requests succeeded. If any request fails the
+   * callbacks' {@code onFailure} method is invoked. In a failure case it can be
+   * that still some of the keys were successfully deleted.
+   */
+  public static void deleteSshKeys(String account,
+      Set<Integer> sequenceNumbers, AsyncCallback<VoidResult> cb) {
+    CallbackGroup group = new CallbackGroup();
+    for (int seq : sequenceNumbers) {
+      new RestApi("/accounts/").id(account).view("sshkeys").id(seq)
+          .delete(group.add(cb));
+      cb = CallbackGroup.emptyCallback();
+    }
+    group.done();
+  }
+
+  /** Retrieve the HTTP password */
+  public static void getHttpPassword(String account,
+      AsyncCallback<NativeString> cb) {
+    new RestApi("/accounts/").id(account).view("password.http").get(cb);
+  }
+
+  /** Generate a new HTTP password */
+  public static void generateHttpPassword(String account,
+      AsyncCallback<NativeString> cb) {
+    HttpPasswordInput in = HttpPasswordInput.create();
+    in.generate(true);
+    new RestApi("/accounts/").id(account).view("password.http").put(in, cb);
+  }
+
+  /** Clear HTTP password */
+  public static void clearHttpPassword(String account,
+      AsyncCallback<VoidResult> cb) {
+    new RestApi("/accounts/").id(account).view("password.http").delete(cb);
+  }
+
+  private static class HttpPasswordInput extends JavaScriptObject {
+    final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/;
+
+    static HttpPasswordInput create() {
+      return createObject().cast();
+    }
+
+    protected HttpPasswordInput() {
+    }
+  }
+}
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 917c078..e0e2d22 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
@@ -26,6 +26,8 @@
   String accountId();
 
   String commentVisibilityLabel();
+  String changeScreenLabel();
+  String diffViewLabel();
   String maximumPageSizeFieldLabel();
   String dateFormatLabel();
   String contextWholeFile();
@@ -37,6 +39,9 @@
   String buttonSaveChanges();
   String showRelativeDateInChangeTable();
 
+  String changeScreenOldUi();
+  String changeScreenNewUi();
+
   String tabAccountSummary();
   String tabPreferences();
   String tabWatchedProjects();
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 7175b6a..333dcfa 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
@@ -8,15 +8,20 @@
 showSiteHeader = Show Site Header
 useFlashClipboard = Use Flash Clipboard Widget
 copySelfOnEmails = CC Me On Comments I Write
-reversePatchSetOrder = Display Patch Sets In Reverse Order
+reversePatchSetOrder = Display Patch Sets In Reverse Order (deprecated: Old Change Screen)
 showUsernameInReviewCategory = Display Person Name In Review Category
 maximumPageSizeFieldLabel = Maximum Page Size:
 commentVisibilityLabel = Comment Visibility:
+changeScreenLabel = Change View:
+diffViewLabel = Diff View (Change Screen 2):
 dateFormatLabel = Date/Time Format:
 contextWholeFile = Whole File
 buttonSaveChanges = Save Changes
 showRelativeDateInChangeTable = Show Relative Dates in Changes Table
 
+changeScreenOldUi = Old Screen
+changeScreenNewUi = Change Screen 2
+
 tabAccountSummary = Profile
 tabPreferences = Preferences
 tabWatchedProjects = Watched Projects
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
index 601d807..26d551f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
@@ -15,12 +15,36 @@
 package com.google.gerrit.client.account;
 
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 
 public class AccountInfo extends JavaScriptObject {
   public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
   public final native String name() /*-{ return this.name; }-*/;
   public final native String email() /*-{ return this.email; }-*/;
 
+  /**
+   * @return true if the server supplied avatar information about this account.
+   *         The information may be an empty list, indicating no avatars are
+   *         available, such as when no plugin is installed. This method returns
+   *         false if the server did not check on avatars for the account.
+   */
+  public final native boolean has_avatar_info()
+  /*-{ return this.hasOwnProperty('avatars') }-*/;
+
+  public final AvatarInfo avatar(int sz) {
+    JsArray<AvatarInfo> a = avatars();
+    for (int i = 0; a != null && i < a.length(); i++) {
+      AvatarInfo r = a.get(i);
+      if (r.height() == sz) {
+        return r;
+      }
+    }
+    return null;
+  }
+
+  private final native JsArray<AvatarInfo> avatars()
+  /*-{ return this.avatars }-*/;
+
   public static native AccountInfo create(int id, String name,
       String email) /*-{
     return {'_account_id': id, 'name': name, 'email': email};
@@ -28,4 +52,14 @@
 
   protected AccountInfo() {
   }
+
+  public static class AvatarInfo extends JavaScriptObject {
+    public final static int DEFAULT_SIZE = 26;
+    public final native String url() /*-{ return this.url }-*/;
+    public final native int height() /*-{ return this.height || 0 }-*/;
+    public final native int width() /*-{ return this.width || 0 }-*/;
+
+    protected AvatarInfo() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
index 2014f19..e55be79 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
@@ -21,6 +21,7 @@
 public interface AccountMessages extends Messages {
   String lines(short cnt);
   String rowsPerPage(short cnt);
+  String changeScreenServerDefault(String d);
   String enterIAGREE(String iagree);
   String contactOnFile(Date lastDate);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
index d013911..313893e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
@@ -1,5 +1,7 @@
 lines = {0} lines
 rowsPerPage = {0} rows per page
 
+changeScreenServerDefault = Server Default ({0}) 
+
 enterIAGREE = (enter {0} in the box to the left)
 contactOnFile = Contact information last updated on {0,date,medium} at {0,time,short}.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index f35bd4b..66f676e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -17,14 +17,15 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.ContactInformation;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -45,12 +46,6 @@
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
 class ContactPanelShort extends Composite {
   protected final FlowPanel body;
   protected int labelIdx, fieldIdx;
@@ -150,7 +145,6 @@
         doSave(null);
       }
     });
-    new OnEditEnabler(save, nameTxt);
 
     emailPick.addChangeHandler(new ChangeHandler() {
       @Override
@@ -209,28 +203,19 @@
         postLoad();
       }
     });
-    Util.ACCOUNT_SEC
-        .myExternalIds(new GerritCallback<List<AccountExternalId>>() {
-          public void onSuccess(final List<AccountExternalId> result) {
-            if (!isAttached()) {
-              return;
-            }
-            final Set<String> emails = new HashSet<String>();
-            for (final AccountExternalId i : result) {
-              if (i.getEmailAddress() != null
-                  && i.getEmailAddress().length() > 0) {
-                emails.add(i.getEmailAddress());
-              }
-            }
-            final List<String> addrs = new ArrayList<String>(emails);
-            Collections.sort(addrs);
-            for (String s : addrs) {
-              emailPick.addItem(s);
-            }
-            haveEmails = true;
-            postLoad();
-          }
-        });
+    AccountApi.getEmails("self", new GerritCallback<JsArray<EmailInfo>>() {
+      @Override
+      public void onSuccess(JsArray<EmailInfo> result) {
+        if (!isAttached()) {
+          return;
+        }
+        for (EmailInfo i : Natives.asList(result)) {
+          emailPick.addItem(i.email());
+        }
+        haveEmails = true;
+        postLoad();
+      }
+    });
   }
 
   private void postLoad() {
@@ -255,6 +240,7 @@
     currentEmail = userAccount.getPreferredEmail();
     nameTxt.setText(userAccount.getFullName());
     save.setEnabled(false);
+    new OnEditEnabler(save, nameTxt);
   }
 
   private void doRegisterNewEmail() {
@@ -283,13 +269,16 @@
 
         inEmail.setEnabled(false);
         register.setEnabled(false);
-        Util.ACCOUNT_SEC.registerEmail(addr, new GerritCallback<Account>() {
-          public void onSuccess(Account currentUser) {
+        AccountApi.registerEmail("self", addr, new GerritCallback<EmailInfo>() {
+          @Override
+          public void onSuccess(EmailInfo result) {
             box.hide();
             if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
               currentEmail = addr;
               if (emailPick.getItemCount() == 0) {
-                onSaveSuccess(currentUser);
+                final Account me = Gerrit.getUserAccount();
+                me.setPreferredEmail(addr);
+                onSaveSuccess(me);
               } else {
                 save.setEnabled(true);
               }
@@ -419,6 +408,14 @@
       }
     }
     if (emailPick.getItemCount() > 0) {
+      if (currentEmail == null) {
+        int index = emailListIndexOf("");
+        if (index != -1) {
+          emailPick.removeItem(index);
+        }
+        emailPick.insertItem("", 0);
+        emailPick.setSelectedIndex(0);
+      }
       emailPick.setVisible(true);
       emailPick.setEnabled(true);
       if (canRegisterNewEmail()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java
new file mode 100644
index 0000000..d0bbd8c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class EmailInfo extends JavaScriptObject {
+  public final native String email() /*-{ return this.email; }-*/;
+  public final native boolean isPreferred() /*-{ return this['preferred'] ? true : false; }-*/;
+  public final native boolean isConfirmationPending() /*-{ return this['pending_confirmation'] ? true : false; }-*/;
+
+  protected EmailInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
index abb1f4d..72ea795 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.client.account;
 
-import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
-
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.i18n.client.LocaleInfo;
@@ -31,13 +31,10 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
-import java.util.List;
-
 public class MyPasswordScreen extends SettingsScreen {
   private CopyableLabel password;
   private Button generatePassword;
   private Button clearPassword;
-  private AccountExternalId id;
 
   @Override
   protected void onInitUI() {
@@ -101,37 +98,48 @@
     }
 
     enableUI(false);
-    Util.ACCOUNT_SEC
-        .myExternalIds(new ScreenLoadCallback<List<AccountExternalId>>(this) {
-          public void preDisplay(final List<AccountExternalId> result) {
-            AccountExternalId id = null;
-            for (AccountExternalId i : result) {
-              if (i.isScheme(SCHEME_USERNAME)) {
-                id = i;
-                break;
-              }
-            }
-            display(id);
-          }
-        });
+    AccountApi.getUsername("self", new GerritCallback<NativeString>() {
+      @Override
+      public void onSuccess(NativeString user) {
+        Gerrit.getUserAccount().setUserName(user.asString());
+        refreshHttpPassword();
+      }
+
+      @Override
+      public void onFailure(final Throwable caught) {
+        if (RestApi.isNotFound(caught)) {
+          Gerrit.getUserAccount().setUserName(null);
+          display();
+        } else {
+          super.onFailure(caught);
+        }
+      }
+    });
   }
 
-  private void display(AccountExternalId id) {
-    String user, pass;
-    if (id != null) {
-      user = id.getSchemeRest();
-      pass = id.getPassword();
-    } else {
-      user = null;
-      pass = null;
-    }
-    this.id = id;
+  private void refreshHttpPassword() {
+    AccountApi.getHttpPassword("self", new ScreenLoadCallback<NativeString>(
+        this) {
+      @Override
+      protected void preDisplay(NativeString httpPassword) {
+        display(httpPassword.asString());
+      }
 
-    Gerrit.getUserAccount().setUserName(user);
+      @Override
+      public void onFailure(final Throwable caught) {
+        if (RestApi.isNotFound(caught)) {
+          display(null);
+          display();
+        } else {
+          super.onFailure(caught);
+        }
+      }
+    });
+  }
 
+  private void display(String pass) {
     password.setText(pass != null ? pass : "");
     password.setVisible(pass != null);
-
     enableUI(true);
   }
 
@@ -150,12 +158,13 @@
   }
 
   private void doGeneratePassword() {
-    if (id != null) {
+    if (Gerrit.getUserAccount().getUserName() != null) {
       enableUI(false);
-      Util.ACCOUNT_SEC.generatePassword(id.getKey(),
-          new GerritCallback<AccountExternalId>() {
-            public void onSuccess(final AccountExternalId result) {
-              display(result);
+      AccountApi.generateHttpPassword("self",
+          new GerritCallback<NativeString>() {
+            @Override
+            public void onSuccess(NativeString newPassword) {
+              display(newPassword.asString());
             }
 
             @Override
@@ -167,12 +176,13 @@
   }
 
   private void doClearPassword() {
-    if (id != null) {
+    if (Gerrit.getUserAccount().getUserName() != null) {
       enableUI(false);
-      Util.ACCOUNT_SEC.clearPassword(id.getKey(),
-          new GerritCallback<AccountExternalId>() {
-            public void onSuccess(final AccountExternalId result) {
-              display(result);
+      AccountApi.clearHttpPassword("self",
+          new GerritCallback<VoidResult>() {
+            @Override
+            public void onSuccess(VoidResult result) {
+              display(null);
             }
 
             @Override
@@ -184,9 +194,9 @@
   }
 
   private void enableUI(boolean on) {
-    on &= id != null;
+    on &= Gerrit.getUserAccount().getUserName() != null;
 
     generatePassword.setEnabled(on);
-    clearPassword.setVisible(on && id.getPassword() != null);
+    clearPassword.setVisible(on && !"".equals(password.getText()));
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 639a1cf..731eaeb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DEFAULT_PAGESIZE;
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.PAGESIZE_CHOICES;
 
+import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
@@ -48,6 +49,8 @@
   private ListBox dateFormat;
   private ListBox timeFormat;
   private ListBox commentVisibilityStrategy;
+  private ListBox changeScreen;
+  private ListBox diffView;
   private Button save;
 
   @Override
@@ -67,20 +70,36 @@
     commentVisibilityStrategy = new ListBox();
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageCollapseAll(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.COLLAPSE_ALL.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.COLLAPSE_ALL.name());
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageExpandMostRecent(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT.name());
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageExpandRecent(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT.name());
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageExpandAll(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_ALL.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_ALL.name());
+
+    changeScreen = new ListBox();
+    changeScreen.addItem(
+        Util.M.changeScreenServerDefault(
+            getLabel(Gerrit.getConfig().getChangeScreen())),
+        "");
+    changeScreen.addItem(
+        Util.C.changeScreenOldUi(),
+        AccountGeneralPreferences.ChangeScreen.OLD_UI.name());
+    changeScreen.addItem(
+        Util.C.changeScreenNewUi(),
+        AccountGeneralPreferences.ChangeScreen.CHANGE_SCREEN2.name());
+
+    diffView = new ListBox();
+    diffView.addItem(
+        com.google.gerrit.client.changes.Util.C.sideBySide(),
+        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE.name());
+    diffView.addItem(
+        com.google.gerrit.client.changes.Util.C.unifiedDiff(),
+        AccountGeneralPreferences.DiffView.UNIFIED_DIFF.name());
 
     Date now = new Date();
     dateFormat = new ListBox();
@@ -118,7 +137,7 @@
 
     relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
 
-    final Grid formGrid = new Grid(9, 2);
+    final Grid formGrid = new Grid(11, 2);
 
     int row = 0;
     formGrid.setText(row, labelIdx, "");
@@ -157,6 +176,14 @@
     formGrid.setWidget(row, fieldIdx, commentVisibilityStrategy);
     row++;
 
+    formGrid.setText(row, labelIdx, Util.C.changeScreenLabel());
+    formGrid.setWidget(row, fieldIdx, changeScreen);
+    row++;
+
+    formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
+    formGrid.setWidget(row, fieldIdx, diffView);
+    row++;
+
     add(formGrid);
 
     save = new Button(Util.C.buttonSaveChanges());
@@ -180,6 +207,8 @@
     e.listenTo(timeFormat);
     e.listenTo(relativeDateInChangeTable);
     e.listenTo(commentVisibilityStrategy);
+    e.listenTo(changeScreen);
+    e.listenTo(diffView);
   }
 
   @Override
@@ -203,6 +232,8 @@
     timeFormat.setEnabled(on);
     relativeDateInChangeTable.setEnabled(on);
     commentVisibilityStrategy.setEnabled(on);
+    changeScreen.setEnabled(on);
+    diffView.setEnabled(on);
   }
 
   private void display(final AccountGeneralPreferences p) {
@@ -218,8 +249,14 @@
         p.getTimeFormat());
     relativeDateInChangeTable.setValue(p.isRelativeDateInChangeTable());
     setListBox(commentVisibilityStrategy,
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT,
         p.getCommentVisibilityStrategy());
+    setListBox(changeScreen,
+        null,
+        p.getChangeScreen());
+    setListBox(diffView,
+        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
+        p.getDiffView());
   }
 
   private void setListBox(final ListBox f, final short defaultValue,
@@ -229,7 +266,8 @@
 
   private <T extends Enum<?>> void setListBox(final ListBox f,
       final T defaultValue, final T currentValue) {
-    setListBox(f, defaultValue.name(), //
+    setListBox(f,
+        defaultValue != null ? defaultValue.name() : "",
         currentValue != null ? currentValue.name() : "");
   }
 
@@ -260,6 +298,9 @@
     final int idx = f.getSelectedIndex();
     if (0 <= idx) {
       String v = f.getValue(idx);
+      if ("".equals(v)) {
+        return defaultValue;
+      }
       for (T t : all) {
         if (t.name().equals(v)) {
           return t;
@@ -285,8 +326,14 @@
         AccountGeneralPreferences.TimeFormat.values()));
     p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue());
     p.setCommentVisibilityStrategy(getListBox(commentVisibilityStrategy,
-        CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+        CommentVisibilityStrategy.EXPAND_RECENT,
         CommentVisibilityStrategy.values()));
+    p.setDiffView(getListBox(diffView,
+        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
+        AccountGeneralPreferences.DiffView.values()));
+    p.setChangeScreen(getListBox(changeScreen,
+        null,
+        AccountGeneralPreferences.ChangeScreen.values()));
 
     enable(false);
     save.setEnabled(false);
@@ -296,6 +343,7 @@
       public void onSuccess(final VoidResult result) {
         Gerrit.getUserAccount().setGeneralPreferences(p);
         Gerrit.applyUserPreferences();
+        Dispatcher.changeScreen2 = false;
         enable(true);
       }
 
@@ -307,4 +355,18 @@
       }
     });
   }
+
+  private static String getLabel(AccountGeneralPreferences.ChangeScreen ui) {
+    if (ui == null) {
+      return "";
+    }
+    switch (ui) {
+      case OLD_UI:
+        return Util.C.changeScreenOldUi();
+      case CHANGE_SCREEN2:
+        return Util.C.changeScreenNewUi();
+      default:
+        return ui.name();
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
index e228647..9f248ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -49,7 +48,7 @@
     fmt.setRowSpan(0, 0, 2);
     fmt.setRowSpan(0, 1, 2);
     fmt.setRowSpan(0, 2, 2);
-    DOM.setElementProperty(fmt.getElement(0, 3), "align", "center");
+    fmt.getElement(0, 3).setPropertyString("align", "center");
 
     fmt.setColSpan(0, 3, 5);
     table.setText(1, 0, Util.C.watchedProjectColumnNewChanges());
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 97b2efb..2c29ab2 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
@@ -29,7 +29,9 @@
     if (Gerrit.getConfig().getSshdAddress() != null) {
       link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
     }
-    link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
+    if (!Gerrit.getConfig().isGitBasicAuth()) {
+      link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
+    }
     link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
     link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
     if (Gerrit.getConfig().isUseContributorAgreements()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java
new file mode 100644
index 0000000..a4fed8b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class SshKeyInfo extends JavaScriptObject {
+  public final native int seq() /*-{ return this.seq || 0; }-*/;
+  public final native String sshPublicKey() /*-{ return this.ssh_public_key; }-*/;
+  public final native String encodedKey() /*-{ return this.encoded_key; }-*/;
+  public final native String algorithm() /*-{ return this.algorithm; }-*/;
+  public final native String comment() /*-{ return this.comment; }-*/;
+  public final native boolean isValid() /*-{ return this['valid'] ? true : false; }-*/;
+
+  protected SshKeyInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
index aaadc5e6..0dfb520 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
@@ -16,13 +16,15 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
@@ -40,7 +42,6 @@
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -157,11 +158,11 @@
     final String txt = addTxt.getText();
     if (txt != null && txt.length() > 0) {
       addNew.setEnabled(false);
-      Util.ACCOUNT_SEC.addSshKey(txt, new GerritCallback<AccountSshKey>() {
-        public void onSuccess(final AccountSshKey result) {
+      AccountApi.addSshKey("self", txt, new GerritCallback<SshKeyInfo>() {
+        public void onSuccess(final SshKeyInfo k) {
           addNew.setEnabled(true);
           addTxt.setText("");
-          keys.addOneKey(result);
+          keys.addOneKey(k);
           if (!keys.isVisible()) {
             showAddKeyBlock(false);
             setKeyTableVisible(true);
@@ -195,24 +196,27 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-
-    Util.ACCOUNT_SEC.mySshKeys(new GerritCallback<List<AccountSshKey>>() {
-      public void onSuccess(final List<AccountSshKey> result) {
-        keys.display(result);
-        if (result.isEmpty() && keys.isVisible()) {
-          showAddKeyBlock(true);
+    refreshSshKeys();
+    Gerrit.SYSTEM_SVC.daemonHostKeys(new GerritCallback<List<SshHostKey>>() {
+      public void onSuccess(final List<SshHostKey> result) {
+        serverKeys.clear();
+        for (final SshHostKey keyInfo : result) {
+          serverKeys.add(new SshHostKeyPanel(keyInfo));
         }
         if (++loadCount == 2) {
           display();
         }
       }
     });
+  }
 
-    Gerrit.SYSTEM_SVC.daemonHostKeys(new GerritCallback<List<SshHostKey>>() {
-      public void onSuccess(final List<SshHostKey> result) {
-        serverKeys.clear();
-        for (final SshHostKey keyInfo : result) {
-          serverKeys.add(new SshHostKeyPanel(keyInfo));
+  private void refreshSshKeys() {
+    AccountApi.getSshKeys("self", new GerritCallback<JsArray<SshKeyInfo>>() {
+      @Override
+      public void onSuccess(JsArray<SshKeyInfo> result) {
+        keys.display(Natives.asList(result));
+        if (result.length() == 0 && keys.isVisible()) {
+          showAddKeyBlock(true);
         }
         if (++loadCount == 2) {
           display();
@@ -229,7 +233,7 @@
     addKeyBlock.setVisible(show);
   }
 
-  private class SshKeyTable extends FancyFlexTable<AccountSshKey> {
+  private class SshKeyTable extends FancyFlexTable<SshKeyInfo> {
     private ValueChangeHandler<Boolean> updateDeleteHandler;
 
     SshKeyTable() {
@@ -255,44 +259,53 @@
     }
 
     void deleteChecked() {
-      final HashSet<AccountSshKey.Id> ids = new HashSet<AccountSshKey.Id>();
+      final HashSet<Integer> sequenceNumbers = new HashSet<Integer>();
       for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountSshKey k = getRowItem(row);
+        final SshKeyInfo k = getRowItem(row);
         if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          ids.add(k.getKey());
+          sequenceNumbers.add(k.seq());
         }
       }
-      if (ids.isEmpty()) {
+      if (sequenceNumbers.isEmpty()) {
         updateDeleteButton();
       } else {
-        Util.ACCOUNT_SEC.deleteSshKeys(ids, new GerritCallback<VoidResult>() {
-          public void onSuccess(final VoidResult result) {
-            for (int row = 1; row < table.getRowCount();) {
-              final AccountSshKey k = getRowItem(row);
-              if (k != null && ids.contains(k.getKey())) {
-                table.removeRow(row);
-              } else {
-                row++;
+        deleteKey.setEnabled(false);
+        AccountApi.deleteSshKeys("self", sequenceNumbers,
+            new GerritCallback<VoidResult>() {
+              public void onSuccess(VoidResult result) {
+                for (int row = 1; row < table.getRowCount();) {
+                  final SshKeyInfo k = getRowItem(row);
+                  if (k != null && sequenceNumbers.contains(k.seq())) {
+                    table.removeRow(row);
+                  } else {
+                    row++;
+                  }
+                }
+                if (table.getRowCount() == 1) {
+                  display(Collections.<SshKeyInfo> emptyList());
+                } else {
+                  updateDeleteButton();
+                }
               }
-            }
-            if (table.getRowCount() == 1) {
-              display(Collections.<AccountSshKey> emptyList());
-            } else {
-              updateDeleteButton();
-            }
-          }
-        });
+
+              @Override
+              public void onFailure(Throwable caught) {
+                refreshSshKeys();
+                updateDeleteButton();
+                super.onFailure(caught);
+              }
+            });
       }
     }
 
-    void display(final List<AccountSshKey> result) {
+    void display(final List<SshKeyInfo> result) {
       if (result.isEmpty()) {
         setKeyTableVisible(false);
         showAddKeyBlock(true);
       } else {
         while (1 < table.getRowCount())
           table.removeRow(table.getRowCount() - 1);
-        for (final AccountSshKey k : result) {
+        for (final SshKeyInfo k : result) {
           addOneKey(k);
         }
         setKeyTableVisible(true);
@@ -300,7 +313,7 @@
       }
     }
 
-    void addOneKey(final AccountSshKey k) {
+    void addOneKey(final SshKeyInfo k) {
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
       final int row = table.getRowCount();
       table.insertRow(row);
@@ -318,13 +331,13 @@
         table.setText(row, 2, Util.C.sshKeyInvalid());
         fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().sshKeyPanelInvalid());
       }
-      table.setText(row, 3, k.getAlgorithm());
+      table.setText(row, 3, k.algorithm());
 
-      CopyableLabel keyLabel = new CopyableLabel(k.getSshPublicKey());
-      keyLabel.setPreviewText(elide(k.getEncodedKey(), 40));
+      CopyableLabel keyLabel = new CopyableLabel(k.sshPublicKey());
+      keyLabel.setPreviewText(elide(k.encodedKey(), 40));
       table.setWidget(row, 4, keyLabel);
 
-      table.setText(row, 5, k.getComment());
+      table.setText(row, 5, k.comment());
 
       fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
       fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().sshKeyPanelEncodedKey());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
new file mode 100644
index 0000000..7f13075
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.actions;
+
+import com.google.gerrit.client.api.ActionContext;
+import com.google.gerrit.client.api.ChangeGlue;
+import com.google.gerrit.client.api.ProjectGlue;
+import com.google.gerrit.client.api.RevisionGlue;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+public class ActionButton extends Button implements ClickHandler {
+  private final Project.NameKey project;
+  private final ChangeInfo change;
+  private final RevisionInfo revision;
+  private final ActionInfo action;
+  private ActionContext ctx;
+
+  public ActionButton(Project.NameKey project, ActionInfo action) {
+    this(project, null, null, action);
+  }
+
+  public ActionButton(ChangeInfo change, ActionInfo action) {
+    this(change, null, action);
+  }
+
+  public ActionButton(ChangeInfo change, RevisionInfo revision,
+      ActionInfo action) {
+    this(null, change, revision, action);
+  }
+
+  private ActionButton(Project.NameKey project, ChangeInfo change,
+      RevisionInfo revision, ActionInfo action) {
+    super(new SafeHtmlBuilder()
+      .openDiv()
+      .append(action.label())
+      .closeDiv());
+    setStyleName("");
+    setTitle(action.title());
+    setEnabled(action.enabled());
+    addClickHandler(this);
+
+    this.project = project;
+    this.change = change;
+    this.revision = revision;
+    this.action = action;
+  }
+
+  @Override
+  public void onClick(ClickEvent event) {
+    if (ctx != null && ctx.has_popup()) {
+      ctx.hide();
+      ctx = null;
+      return;
+    }
+
+    if (revision != null) {
+      RevisionGlue.onAction(change, revision, action, this);
+    } else if (change != null) {
+      ChangeGlue.onAction(change, action, this);
+    } else if (project != null) {
+      ProjectGlue.onAction(project, action, this);
+    }
+  }
+
+  @Override
+  public void onUnload() {
+    if (ctx != null) {
+      if (ctx.has_popup()) {
+        ctx.hide();
+      }
+      ctx = null;
+    }
+    super.onUnload();
+  }
+
+  public void link(ActionContext ctx) {
+    this.ctx = ctx;
+  }
+
+  public void unlink() {
+    ctx = null;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java
new file mode 100644
index 0000000..ef0c4b5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.actions;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ActionInfo extends JavaScriptObject {
+
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String method() /*-{ return this.method; }-*/;
+  public final native String label() /*-{ return this.label; }-*/;
+  public final native String title() /*-{ return this.title; }-*/;
+  public final native boolean enabled() /*-{ return this.enabled || false; }-*/;
+
+  protected ActionInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 344104d..b734b41 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -93,9 +93,8 @@
 
   public AccessSectionEditor(ProjectAccess access) {
     projectAccess = access;
-
-    permissionSelector =
-        new ValueListBox<String>(PermissionNameRenderer.INSTANCE);
+    permissionSelector = new ValueListBox<String>(
+        new PermissionNameRenderer(access.getCapabilities()));
     permissionSelector.addValueChangeHandler(new ValueChangeHandler<String>() {
       @Override
       public void onValueChange(ValueChangeEvent<String> event) {
@@ -222,12 +221,15 @@
     List<String> perms = new ArrayList<String>();
 
     if (AccessSection.GLOBAL_CAPABILITIES.equals(value.getName())) {
-      for (String varName : Util.C.capabilityNames().keySet()) {
+      for (String varName : projectAccess.getCapabilities().keySet()) {
         addPermission(varName, perms);
       }
     } else if (RefConfigSection.isValid(value.getName())) {
       for (LabelType t : projectAccess.getLabelTypes().getLabelTypes()) {
-        addPermission(Permission.LABEL + t.getName(), perms);
+        addPermission(Permission.forLabel(t.getName()), perms);
+      }
+      for (LabelType t : projectAccess.getLabelTypes().getLabelTypes()) {
+        addPermission(Permission.forLabelAs(t.getName()), perms);
       }
       for (String varName : Util.C.permissionNames().keySet()) {
         addPermission(varName, perms);
@@ -282,7 +284,7 @@
     @Override
     public PermissionEditor create(int index) {
       PermissionEditor subEditor =
-          new PermissionEditor(projectAccess.getProjectName(), readOnly, value,
+          new PermissionEditor(projectAccess, readOnly, value,
               projectAccess.getLabelTypes());
       permissionContainer.insert(subEditor, index);
       return subEditor;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index ea836c6..8e0d22b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -109,8 +109,6 @@
     });
     groupNamePanel.add(saveName);
     add(groupNamePanel);
-
-    new OnEditEnabler(saveName, groupNameTxt);
   }
 
   private void initOwner() {
@@ -120,8 +118,9 @@
 
     ownerTxtBox = new NpTextBox();
     ownerTxtBox.setVisibleLength(60);
+    final AccountGroupSuggestOracle accountGroupOracle = new AccountGroupSuggestOracle();
     ownerTxt = new SuggestBox(new RPCSuggestOracle(
-        new AccountGroupSuggestOracle()), ownerTxtBox);
+        accountGroupOracle), ownerTxtBox);
     ownerTxt.setStyleName(Gerrit.RESOURCES.css().groupOwnerTextBox());
     ownerPanel.add(ownerTxt);
 
@@ -132,7 +131,9 @@
       public void onClick(final ClickEvent event) {
         final String newOwner = ownerTxt.getText().trim();
         if (newOwner.length() > 0) {
-          GroupApi.setGroupOwner(getGroupUUID(), newOwner,
+          AccountGroup.UUID ownerUuid = accountGroupOracle.getUUID(newOwner);
+          String ownerId = ownerUuid != null ? ownerUuid.get() : newOwner;
+          GroupApi.setGroupOwner(getGroupUUID(), ownerId,
               new GerritCallback<GroupInfo>() {
                 public void onSuccess(final GroupInfo result) {
                   updateOwnerGroup(result);
@@ -144,8 +145,6 @@
     });
     ownerPanel.add(saveOwner);
     add(ownerPanel);
-
-    new OnEditEnabler(saveOwner, ownerTxtBox);
   }
 
   private void initDescription() {
@@ -174,8 +173,6 @@
     });
     vp.add(saveDesc);
     add(vp);
-
-    new OnEditEnabler(saveDesc, descTxt);
   }
 
   private void initGroupOptions() {
@@ -225,5 +222,8 @@
     saveOwner.setVisible(canModify);
     saveDesc.setVisible(canModify);
     saveGroupOptions.setVisible(canModify);
+    new OnEditEnabler(saveDesc, descTxt);
+    new OnEditEnabler(saveName, groupNameTxt);
+    new OnEditEnabler(saveOwner, ownerTxtBox);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 51ff978..0f326bf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -42,8 +43,6 @@
 import java.util.HashSet;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 public class AccountGroupMembersScreen extends AccountGroupScreen {
 
   private MemberTable members;
@@ -58,6 +57,7 @@
   private Button delInclude;
 
   private FlowPanel noMembersInfo;
+  private AccountGroupSuggestOracle accountGroupSuggestOracle;
 
   public AccountGroupMembersScreen(final GroupInfo toShow, final String token) {
     super(toShow, token);
@@ -109,9 +109,10 @@
   }
 
   private void initIncludeList() {
+    accountGroupSuggestOracle = new AccountGroupSuggestOracle();
     addIncludeBox =
         new AddMemberBox(Util.C.buttonAddIncludedGroup(),
-            Util.C.defaultAccountGroupName(), new AccountGroupSuggestOracle());
+            Util.C.defaultAccountGroupName(), accountGroupSuggestOracle);
 
     addIncludeBox.addClickHandler(new ClickHandler() {
       @Override
@@ -187,13 +188,18 @@
   }
 
   void doAddNewInclude() {
-    final String groupName = addIncludeBox.getText();
+    String groupName = addIncludeBox.getText();
     if (groupName.length() == 0) {
       return;
     }
 
+    AccountGroup.UUID uuid = accountGroupSuggestOracle.getUUID(groupName);
+    if (uuid == null) {
+      return;
+    }
+
     addIncludeBox.setEnabled(false);
-    GroupApi.addIncludedGroup(getGroupUUID(), groupName,
+    GroupApi.addIncludedGroup(getGroupUUID(), uuid.get(),
         new GerritCallback<GroupInfo>() {
           public void onSuccess(final GroupInfo result) {
             addIncludeBox.setEnabled(true);
@@ -290,28 +296,12 @@
           return str == null ? "" : str;
         }
       };
-      int insertPosition = table.getRowCount();
-      int left = 1;
-      int right = table.getRowCount() - 1;
-      while (left <= right) {
-        int middle = (left + right) >>> 1; // (left+right)/2
-        AccountInfo i = getRowItem(middle);
-        int cmp = c.compare(i, info);
-
-        if (cmp < 0) {
-          left = middle + 1;
-        } else if (cmp > 0) {
-          right = middle - 1;
-        } else {
-          // group is already contained in the table
-          return;
-        }
+      int insertPos = getInsertRow(c, info);
+      if (insertPos >= 0) {
+        table.insertRow(insertPos);
+        applyDataRowStyle(insertPos);
+        populate(insertPos, info);
       }
-      insertPosition = left;
-
-      table.insertRow(insertPosition);
-      applyDataRowStyle(insertPosition);
-      populate(insertPosition, info);
     }
 
     void populate(final int row, final AccountInfo i) {
@@ -405,29 +395,12 @@
           return (str == null) ? "" : str;
         }
       };
-
-      int insertPosition = table.getRowCount();
-      int left = 1;
-      int right = table.getRowCount() - 1;
-      while (left <= right) {
-        int middle = (left + right) >>> 1; // (left+right)/2
-        GroupInfo i = getRowItem(middle);
-        int cmp = c.compare(i, info);
-
-        if (cmp < 0) {
-          left = middle + 1;
-        } else if (cmp > 0) {
-          right = middle - 1;
-        } else {
-          // group is already contained in the table
-          return;
-        }
+      int insertPos = getInsertRow(c, info);
+      if (insertPos >= 0) {
+        table.insertRow(insertPos);
+        applyDataRowStyle(insertPos);
+        populate(insertPos, info);
       }
-      insertPosition = left;
-
-      table.insertRow(insertPosition);
-      applyDataRowStyle(insertPosition);
-      populate(insertPosition, info);
     }
 
     void populate(final int row, final GroupInfo i) {
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 f4c0b55..7affd1c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -42,6 +42,7 @@
   String useContributorAgreements();
   String useSignedOffBy();
   String requireChangeID();
+  String headingMaxObjectSizeLimit();
   String headingGroupOptions();
   String isVisibleToAll();
   String buttonSaveGroupOptions();
@@ -55,6 +56,8 @@
   String headingOwner();
   String headingDescription();
   String headingProjectOptions();
+  String headingProjectCommands();
+  String headingCommands();
   String headingMembers();
   String headingIncludedGroups();
   String noMembersInfo();
@@ -89,7 +92,6 @@
   String initialRevision();
   String buttonAddBranch();
   String buttonDeleteBranch();
-  String branchDeletionOpenChanges();
 
   String groupItemHelp();
 
@@ -125,8 +127,6 @@
   String refErrorPrintable();
   String errorsMustBeFixed();
 
-  Map<String, String> capabilityNames();
-
   String sectionTypeReference();
   String sectionTypeSection();
   Map<String, String> sectionNames();
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 178db6b..fa1dc87 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
@@ -24,6 +24,7 @@
 useContributorAgreements = Require a valid contributor agreement to upload
 useSignedOffBy = Require <code>Signed-off-by</code> in commit message
 requireChangeID = Require <code>Change-Id</code> in commit message
+headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
 isVisibleToAll = Make group visible to all registered users.
 buttonSaveGroupOptions = Save Group Options
@@ -36,6 +37,8 @@
 headingOwner = Owners
 headingDescription = Description
 headingProjectOptions = Project Options
+headingProjectCommands = Project Commands
+headingCommands = Commands
 headingMembers = Members
 headingIncludedGroups = Included Groups
 noMembersInfo = Group Members can only be viewed for Gerrit internal groups. For external groups and Gerrit system groups the members cannot be displayed.
@@ -68,8 +71,6 @@
 initialRevision = Initial Revision
 buttonAddBranch = Create Branch
 buttonDeleteBranch = Delete
-branchDeletionOpenChanges = The following branches were not deleted \
-because they have open changes:
 
 groupItemHelp = group
 
@@ -144,41 +145,6 @@
 refErrorPrintable = References may contain only printable characters
 errorsMustBeFixed = Errors must be fixed before committing changes.
 
-# Capability Names
-capabilityNames = \
-  accessDatabase, \
-  administrateServer, \
-  createAccount, \
-  createGroup, \
-  createProject, \
-  emailReviewers, \
-  flushCaches, \
-  killTask, \
-  priority, \
-  queryLimit, \
-  runGC, \
-  startReplication, \
-  streamEvents, \
-  viewCaches, \
-  viewConnections, \
-  viewQueue
-accessDatabase = Access Database
-administrateServer = Administrate Server
-createAccount = Create Account
-createGroup = Create Group
-createProject = Create Project
-emailReviewers = Email Reviewers
-flushCaches = Flush Caches
-killTask = Kill Task
-priority = Priority
-queryLimit = Query Limit
-runGC = Run Garbage Collection
-startReplication = Start Replication
-streamEvents = Stream Events
-viewCaches = View Caches
-viewConnections = View Connections
-viewQueue = View Queue
-
 # Section Names
 sectionTypeReference = Reference:
 sectionTypeSection = Section:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
index 3b4d7d4..6b1269d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
@@ -19,9 +19,13 @@
 public interface AdminMessages extends Messages {
   String group(String name);
   String label(String name);
+  String labelAs(String name);
   String project(String name);
   String deletedGroup(int id);
 
   String deletedReference(String name);
   String deletedSection(String name);
+
+  String effectiveMaxObjectSizeLimit(String effectiveMaxObjectSizeLimit);
+  String globalMaxObjectSizeLimit(String globalMaxObjectSizeLimit);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
index 7f8cd56..cb3784f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
@@ -1,6 +1,9 @@
 group = Group {0}
 label = Label {0}
+labelAs = Label {0} (On Behalf Of)
 project = Project {0}
 deletedGroup = Deleted Group {0}
 deletedReference = Reference {0} was deleted
 deletedSection = Section {0} was deleted
+effectiveMaxObjectSizeLimit = effective: {0}
+globalMaxObjectSizeLimit = The global max object size limit is set to {0}. The limit cannot be increased on project level.
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 dac0b6a..bed6b4a 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
 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;
@@ -55,10 +56,10 @@
   protected void onLoad() {
     super.onLoad();
     display();
-    refresh();
+    refresh(false);
   }
 
-  private void refresh() {
+  private void refresh(final boolean open) {
     setToken(subname == null || "".equals(subname) ? ADMIN_GROUPS
         : ADMIN_GROUPS + "?filter=" + URL.encodeQueryString(subname));
     GroupMap.match(subname,
@@ -66,8 +67,13 @@
             new GerritCallback<GroupMap>() {
               @Override
               public void onSuccess(GroupMap result) {
-                groups.display(result, subname);
-                groups.finishDisplay();
+                if (open && result.values().length() > 0) {
+                  Gerrit.display(PageLinks.toGroup(
+                      result.values().get(0).getGroupUUID()));
+                } else {
+                  groups.display(result, subname);
+                  groups.finishDisplay();
+                }
               }
             }));
   }
@@ -99,7 +105,7 @@
       @Override
       public void onKeyUp(KeyUpEvent event) {
         subname = filterTxt.getValue();
-        refresh();
+        refresh(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
       }
     });
     hp.add(filterTxt);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index 9848c18..5222751 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
@@ -107,17 +108,19 @@
   private PermissionRange.WithDefaults validRange;
   private boolean isDeleted;
 
-  public PermissionEditor(Project.NameKey projectName,
+  public PermissionEditor(ProjectAccess projectAccess,
       boolean readOnly,
       AccessSection section,
       LabelTypes labelTypes) {
     this.readOnly = readOnly;
     this.section = section;
-    this.projectName = projectName;
+    this.projectName = projectAccess.getProjectName();
     this.labelTypes = labelTypes;
 
-    normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
-    deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
+    PermissionNameRenderer nameRenderer =
+        new PermissionNameRenderer(projectAccess.getCapabilities());
+    normalName = new ValueLabel<String>(nameRenderer);
+    deletedName = new ValueLabel<String>(nameRenderer);
 
     initWidget(uiBinder.createAndBindUi(this));
     groupToAdd.setProject(projectName);
@@ -262,7 +265,7 @@
   public void setValue(Permission value) {
     this.value = value;
 
-    if (value.isLabel()) {
+    if (Permission.hasRange(value.getName())) {
       LabelType lt = labelTypes.byLabel(value.getLabel());
       if (lt != null) {
         validRange = new PermissionRange.WithDefaults(
@@ -277,7 +280,7 @@
       validRange = null;
     }
 
-    if (value != null && Permission.OWNER.equals(value.getName())) {
+    if (Permission.OWNER.equals(value.getName())) {
       exclusiveGroup.setEnabled(false);
     } else {
       exclusiveGroup.setEnabled(!readOnly);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
index ad3473c..d8ee195 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
@@ -22,37 +22,54 @@
 import java.util.Map;
 
 class PermissionNameRenderer implements Renderer<String> {
-  static final PermissionNameRenderer INSTANCE = new PermissionNameRenderer();
-
-  private static final Map<String, String> all;
+  private static final Map<String, String> permissions;
 
   static {
-    all = new HashMap<String, String>();
-    for (Map.Entry<String, String> e : Util.C.capabilityNames().entrySet()) {
-      all.put(e.getKey(), e.getValue());
-      all.put(e.getKey().toLowerCase(), e.getValue());
-    }
+    permissions = new HashMap<String, String>();
     for (Map.Entry<String, String> e : Util.C.permissionNames().entrySet()) {
-      all.put(e.getKey(), e.getValue());
-      all.put(e.getKey().toLowerCase(), e.getValue());
+      permissions.put(e.getKey(), e.getValue());
+      permissions.put(e.getKey().toLowerCase(), e.getValue());
     }
   }
 
+  private final Map<String, String> fromServer;
+
+  PermissionNameRenderer(Map<String, String> allFromOutside) {
+    fromServer = allFromOutside;
+  }
+
   @Override
   public String render(String varName) {
-    if (Permission.isLabel(varName)) {
-      return Util.M.label(new Permission(varName).getLabel());
+    if (Permission.isLabelAs(varName)) {
+      return Util.M.labelAs(Permission.extractLabel(varName));
+    } else if (Permission.isLabel(varName)) {
+      return Util.M.label(Permission.extractLabel(varName));
     }
 
-    String desc = all.get(varName);
-    if (desc == null) {
-      desc = all.get(varName.toLowerCase());
+    String desc = permissions.get(varName);
+    if (desc != null) {
+      return desc;
     }
-    return desc != null ? desc : varName;
+
+    desc = fromServer.get(varName);
+    if (desc != null) {
+      return desc;
+    }
+
+    desc = permissions.get(varName.toLowerCase());
+    if (desc != null) {
+      return desc;
+    }
+
+    desc = fromServer.get(varName.toLowerCase());
+    if (desc != null) {
+      return desc;
+    }
+    return varName;
   }
 
   @Override
   public void render(String object, Appendable appendable) throws IOException {
     appendable.append(render(object));
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
index 0c2629c..6c9a9b7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -39,7 +39,6 @@
 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.DOM;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.Composite;
@@ -153,7 +152,7 @@
 
     } else {
       rangeEditor.getStyle().setDisplay(Display.NONE);
-      DOM.setElementPropertyBoolean(action.getElement(), "disabled", readOnly);
+      action.getElement().setPropertyBoolean("disabled", readOnly);
     }
 
     if (readOnly) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
index 9e4b81b..f9f0362 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
@@ -90,7 +90,7 @@
             row,
             1,
             new Anchor(plugin.name(), Gerrit.selfRedirect("/plugins/"
-                + plugin.name() + "/")));
+                + plugin.name() + "/"), "_blank"));
       }
       table.setText(row, 2, plugin.version());
       table.setText(row, 3, plugin.isDisabled() ? Util.C.pluginDisabled()
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index 32bc469..49a9aa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -120,7 +120,7 @@
       history.getStyle().setDisplay(Display.NONE);
     }
 
-    addSection.setVisible(value != null && editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
+    addSection.setVisible(editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 96824f3..ef7f560 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -18,7 +18,12 @@
 import static com.google.gerrit.common.ProjectAccessUtil.removeEmptyPermissionsAndSections;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.config.CapabilityInfo;
+import com.google.gerrit.client.config.ConfigServerApi;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccessSection;
@@ -33,6 +38,7 @@
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.uibinder.client.UiHandler;
 import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Label;
@@ -41,8 +47,10 @@
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class ProjectAccessScreen extends ProjectScreen {
@@ -90,6 +98,8 @@
 
   private ProjectAccess access;
 
+  private NativeMap<CapabilityInfo> capabilityMap;
+
   public ProjectAccessScreen(final Project.NameKey toShow) {
     super(toShow);
   }
@@ -107,18 +117,36 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    CallbackGroup cbs = new CallbackGroup();
+    ConfigServerApi.capabilities(
+        cbs.add(new AsyncCallback<NativeMap<CapabilityInfo>>() {
+          @Override
+          public void onSuccess(NativeMap<CapabilityInfo> result) {
+            capabilityMap = result;
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            // Handled by ScreenLoadCallback.onFailure().
+          }
+        }));
     Util.PROJECT_SVC.projectAccess(getProjectKey(),
-        new ScreenLoadCallback<ProjectAccess>(this) {
+        cbs.addFinal(new ScreenLoadCallback<ProjectAccess>(this) {
           @Override
           public void preDisplay(ProjectAccess access) {
             displayReadOnly(access);
           }
-        });
+        }));
     savedPanel = ACCESS;
   }
 
   private void displayReadOnly(ProjectAccess access) {
     this.access = access;
+    Map<String, String> allCapabilities = new HashMap<String, String>();
+    for (CapabilityInfo c : Natives.asList(capabilityMap.values())) {
+      allCapabilities.put(c.id(), c.name());
+    }
+    this.access.setCapabilities(allCapabilities);
     accessEditor.setEditing(false);
     UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty() || access.canUpload());
     edit.setEnabled(!access.getOwnerOf().isEmpty() || access.canUpload());
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 a7c6a23..04d407e 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
@@ -19,16 +19,19 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.access.AccessMap;
+import com.google.gerrit.client.access.ProjectAccessInfo;
+import com.google.gerrit.client.projects.BranchInfo;
+import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.common.data.AddBranchResult;
-import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -36,23 +39,25 @@
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 public class ProjectBranchesScreen extends ProjectScreen {
-  private BranchesTable branches;
+  private BranchesTable branchTable;
   private Button delBranch;
   private Button addBranch;
   private HintTextBox nameTxtBox;
@@ -66,39 +71,41 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.PROJECT_SVC.listBranches(getProjectKey(),
-        new ScreenLoadCallback<ListBranchesResult>(this) {
+    addPanel.setVisible(false);
+    AccessMap.get(getProjectKey(),
+        new GerritCallback<ProjectAccessInfo>() {
           @Override
-          public void preDisplay(final ListBranchesResult result) {
-            if (result.getNoRepository()) {
-              branches.setVisible(false);
-              addPanel.setVisible(false);
-              delBranch.setVisible(false);
-
-              Label no = new Label(Util.C.errorNoGitRepository());
-              no.setStyleName(Gerrit.RESOURCES.css().smallHeading());
-              add(no);
-
-            } else {
-              enableForm(true);
-              display(result.getBranches());
-              addPanel.setVisible(result.getCanAdd());
-            }
+          public void onSuccess(ProjectAccessInfo result) {
+            addPanel.setVisible(result.canAddRefs());
           }
         });
+    refreshBranches();
     savedPanel = BRANCH;
   }
 
-  private void display(final List<Branch> listBranches) {
-    branches.display(listBranches);
-    delBranch.setVisible(branches.hasBranchCanDelete());
+  private void refreshBranches() {
+    ProjectApi.getBranches(getProjectKey(),
+        new ScreenLoadCallback<JsArray<BranchInfo>>(this) {
+          @Override
+          public void preDisplay(final JsArray<BranchInfo> result) {
+            Set<String> checkedRefs = branchTable.getCheckedRefs();
+            display(Natives.asList(result));
+            branchTable.setChecked(checkedRefs);
+            updateForm();
+          }
+        });
   }
 
-  private void enableForm(final boolean on) {
-    delBranch.setEnabled(on);
-    addBranch.setEnabled(on);
-    nameTxtBox.setEnabled(on);
-    irevTxtBox.setEnabled(on);
+  private void display(final List<BranchInfo> branches) {
+    branchTable.display(branches);
+    delBranch.setVisible(branchTable.hasBranchCanDelete());
+  }
+
+  private void updateForm() {
+    branchTable.updateDeleteButton();
+    addBranch.setEnabled(true);
+    nameTxtBox.setEnabled(true);
+    irevTxtBox.setEnabled(true);
   }
 
   @Override
@@ -149,29 +156,29 @@
     addPanel.add(addGrid);
     addPanel.add(addBranch);
 
-    branches = new BranchesTable();
+    branchTable = new BranchesTable();
 
     delBranch = new Button(Util.C.buttonDeleteBranch());
     delBranch.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        branches.deleteChecked();
+        branchTable.deleteChecked();
       }
     });
 
-    add(branches);
+    add(branchTable);
     add(delBranch);
     add(addPanel);
   }
 
   private void doAddNewBranch() {
-    final String branchName = nameTxtBox.getText();
+    final String branchName = nameTxtBox.getText().trim();
     if ("".equals(branchName)) {
       nameTxtBox.setFocus(true);
       return;
     }
 
-    final String rev = irevTxtBox.getText();
+    final String rev = irevTxtBox.getText().trim();
     if ("".equals(rev)) {
       irevTxtBox.setText("HEAD");
       Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@@ -185,62 +192,23 @@
     }
 
     addBranch.setEnabled(false);
-    Util.PROJECT_SVC.addBranch(getProjectKey(), branchName, rev,
-        new GerritCallback<AddBranchResult>() {
-          public void onSuccess(final AddBranchResult result) {
-            addBranch.setEnabled(true);
-            if (!result.hasError()) {
-              nameTxtBox.setText("");
-              irevTxtBox.setText("");
-              display(result.getListBranchesResult().getBranches());
-            } else {
-              final AddBranchResult.Error error = result.getError();
-              final String msg;
-              switch (error.getType()) {
-                case INVALID_NAME:
-                  selectAllAndFocus(nameTxtBox);
-                  msg = Gerrit.M.invalidBranchName(branchName);
-                  break;
-
-                case INVALID_REVISION:
-                  selectAllAndFocus(irevTxtBox);
-                  msg = Gerrit.M.invalidRevision(rev);
-                  break;
-
-                case BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX:
-                  selectAllAndFocus(nameTxtBox);
-                  msg =
-                      Gerrit.M.branchCreationNotAllowedUnderRefnamePrefix(error
-                          .getRefname());
-                  break;
-
-                case BRANCH_ALREADY_EXISTS:
-                  selectAllAndFocus(nameTxtBox);
-                  msg = Gerrit.M.branchAlreadyExists(error.getRefname());
-                  break;
-
-                case BRANCH_CREATION_CONFLICT:
-                  selectAllAndFocus(nameTxtBox);
-                  msg =
-                      Gerrit.M.branchCreationConflict(branchName,
-                          error.getRefname());
-                  break;
-
-                default:
-                  msg =
-                      Gerrit.M.branchCreationFailed(branchName,
-                          error.toString());
-              }
-              new ErrorDialog(msg).center();
-            }
-          }
-
+    ProjectApi.createBranch(getProjectKey(), branchName, rev,
+        new GerritCallback<BranchInfo>() {
           @Override
-          public void onFailure(final Throwable caught) {
+          public void onSuccess(BranchInfo branch) {
             addBranch.setEnabled(true);
-            super.onFailure(caught);
+            nameTxtBox.setText("");
+            irevTxtBox.setText("");
+            branchTable.insert(branch);
           }
-        });
+
+      @Override
+      public void onFailure(Throwable caught) {
+        addBranch.setEnabled(true);
+        selectAllAndFocus(nameTxtBox);
+        new ErrorDialog(caught.getMessage()).center();
+      }
+    });
   }
 
   private static void selectAllAndFocus(final TextBox textBox) {
@@ -248,7 +216,8 @@
     textBox.setFocus(true);
   }
 
-  private class BranchesTable extends FancyFlexTable<Branch> {
+  private class BranchesTable extends FancyFlexTable<BranchInfo> {
+    private ValueChangeHandler<Boolean> updateDeleteHandler;
     boolean canDelete;
 
     BranchesTable() {
@@ -263,90 +232,108 @@
       if (Gerrit.getGitwebLink() != null) {
         fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
       }
+
+      updateDeleteHandler = new ValueChangeHandler<Boolean>() {
+        @Override
+        public void onValueChange(ValueChangeEvent<Boolean> event) {
+          updateDeleteButton();
+        }
+      };
+    }
+
+    Set<String> getCheckedRefs() {
+      Set<String> refs = new HashSet<String>();
+      for (int row = 1; row < table.getRowCount(); row++) {
+        final BranchInfo k = getRowItem(row);
+        if (k != null && table.getWidget(row, 1) instanceof CheckBox
+            && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+          refs.add(k.ref());
+        }
+      }
+      return refs;
+    }
+
+    void setChecked(Set<String> refs) {
+      for (int row = 1; row < table.getRowCount(); row++) {
+        final BranchInfo k = getRowItem(row);
+        if (k != null && refs.contains(k.ref()) &&
+            table.getWidget(row, 1) instanceof CheckBox) {
+          ((CheckBox) table.getWidget(row, 1)).setValue(true);
+        }
+      }
     }
 
     void deleteChecked() {
-      final SafeHtmlBuilder b = new SafeHtmlBuilder();
+      final Set<String> refs = getCheckedRefs();
+
+      SafeHtmlBuilder b = new SafeHtmlBuilder();
       b.openElement("b");
       b.append(Gerrit.C.branchDeletionConfirmationMessage());
       b.closeElement("b");
 
       b.openElement("p");
-      final HashSet<Branch.NameKey> ids = new HashSet<Branch.NameKey>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final Branch k = getRowItem(row);
-        if (k != null && table.getWidget(row, 1) instanceof CheckBox
-            && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          if (!ids.isEmpty()) {
-            b.append(",").br();
-          }
-          b.append(k.getName());
-          ids.add(k.getNameKey());
+      boolean first = true;
+      for (String ref : refs) {
+        if (!first) {
+          b.append(",").br();
         }
+        b.append(ref);
+        first = false;
       }
       b.closeElement("p");
-      if (ids.isEmpty()) {
+
+      if (refs.isEmpty()) {
+        updateDeleteButton();
         return;
       }
 
+      delBranch.setEnabled(false);
       ConfirmationDialog confirmationDialog =
           new ConfirmationDialog(Gerrit.C.branchDeletionDialogTitle(),
               b.toSafeHtml(), new ConfirmationCallback() {
         @Override
         public void onOk() {
-          deleteBranches(ids);
+          deleteBranches(refs);
+        }
+
+        @Override
+        public void onCancel() {
+          branchTable.updateDeleteButton();
         }
       });
       confirmationDialog.center();
     }
 
-    private void deleteBranches(final Set<Branch.NameKey> branchIds) {
-      Util.PROJECT_SVC.deleteBranch(getProjectKey(), branchIds,
-          new GerritCallback<Set<Branch.NameKey>>() {
-            public void onSuccess(final Set<Branch.NameKey> deleted) {
-              if (!deleted.isEmpty()) {
-                for (int row = 1; row < table.getRowCount();) {
-                  final Branch k = getRowItem(row);
-                  if (k != null && deleted.contains(k.getNameKey())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
+    private void deleteBranches(final Set<String> branches) {
+      ProjectApi.deleteBranches(getProjectKey(), branches,
+          new GerritCallback<VoidResult>() {
+            public void onSuccess(VoidResult result) {
+              for (int row = 1; row < table.getRowCount();) {
+                BranchInfo k = getRowItem(row);
+                if (k != null && branches.contains(k.ref())) {
+                  table.removeRow(row);
+                } else {
+                  row++;
                 }
               }
+              updateDeleteButton();
+            }
 
-              branchIds.removeAll(deleted);
-              if (!branchIds.isEmpty()) {
-                final VerticalPanel p = new VerticalPanel();
-                final ErrorDialog errorDialog = new ErrorDialog(p);
-                final Label l = new Label(Util.C.branchDeletionOpenChanges());
-                l.setStyleName(Gerrit.RESOURCES.css().errorDialogText());
-                p.add(l);
-                for (final Branch.NameKey branch : branchIds) {
-                  final BranchLink link =
-                      new BranchLink(branch.getParentKey(), Change.Status.NEW,
-                          branch.get(), null) {
-                    @Override
-                    public void go() {
-                      errorDialog.hide();
-                      super.go();
-                    };
-                  };
-                  p.add(link);
-                }
-                errorDialog.center();
-              }
+            @Override
+            public void onFailure(Throwable caught) {
+              refreshBranches();
+              super.onFailure(caught);
             }
           });
     }
 
-    void display(final List<Branch> result) {
+    void display(List<BranchInfo> branches) {
       canDelete = false;
 
       while (1 < table.getRowCount())
         table.removeRow(table.getRowCount() - 1);
 
-      for (final Branch k : result) {
+      for (final BranchInfo k : branches) {
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
@@ -354,11 +341,28 @@
       }
     }
 
-    void populate(final int row, final Branch k) {
+    void insert(BranchInfo info) {
+      Comparator<BranchInfo> c = new Comparator<BranchInfo>() {
+        @Override
+        public int compare(BranchInfo a, BranchInfo b) {
+          return a.ref().compareTo(b.ref());
+        }
+      };
+      int insertPos = getInsertRow(c, info);
+      if (insertPos >= 0) {
+        table.insertRow(insertPos);
+        applyDataRowStyle(insertPos);
+        populate(insertPos, info);
+      }
+    }
+
+    void populate(int row, BranchInfo k) {
       final GitwebLink c = Gerrit.getGitwebLink();
 
-      if (k.getCanDelete()) {
-        table.setWidget(row, 1, new CheckBox());
+      if (k.canDelete()) {
+        CheckBox sel = new CheckBox();
+        sel.addValueChangeHandler(updateDeleteHandler);
+        table.setWidget(row, 1, sel);
         canDelete = true;
       } else {
         table.setText(row, 1, "");
@@ -366,15 +370,15 @@
 
       table.setText(row, 2, k.getShortName());
 
-      if (k.getRevision() != null) {
-        table.setText(row, 3, k.getRevision().get());
+      if (k.revision() != null) {
+        table.setText(row, 3, k.revision());
       } else {
         table.setText(row, 3, "");
       }
 
       if (c != null) {
-        table.setWidget(row, 4, new Anchor(c.getLinkName(), false, c.toBranch(k
-            .getNameKey())));
+        table.setWidget(row, 4, new Anchor(c.getLinkName(), false,
+            c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
       }
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
@@ -399,5 +403,20 @@
     boolean hasBranchCanDelete() {
       return canDelete;
     }
+
+    void updateDeleteButton() {
+      boolean on = false;
+      for (int row = 1; row < table.getRowCount(); row++) {
+        Widget w = table.getWidget(row, 1);
+        if (w != null && w instanceof CheckBox) {
+          CheckBox sel = (CheckBox) w;
+          if (sel.getValue()) {
+            on = true;
+            break;
+          }
+        }
+      }
+      delBranch.setEnabled(on);
+    }
   }
 }
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 40921309..f4db4ff 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -15,14 +15,22 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.access.AccessMap;
+import com.google.gerrit.client.access.ProjectAccessInfo;
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.change.Resources;
 import com.google.gerrit.client.download.DownloadPanel;
+import com.google.gerrit.client.projects.ConfigInfo;
+import com.google.gerrit.client.projects.ConfigInfo.InheritedBooleanInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.InheritedBoolean;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
@@ -32,22 +40,28 @@
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
 
 public class ProjectInfoScreen extends ProjectScreen {
-  private String projectName;
-  private Project project;
+  private boolean isOwner;
 
   private LabeledWidgetsGrid grid;
+  private LabeledWidgetsGrid actionsGrid;
 
   // Section: Project Options
   private ListBox requireChangeID;
   private ListBox submitType;
   private ListBox state;
   private ListBox contentMerge;
+  private NpTextBox maxObjectSizeLimit;
+  private Label effectiveMaxObjectSizeLimit;
 
   // Section: Contributor Agreements
   private ListBox contributorAgreements;
@@ -60,13 +74,13 @@
 
   public ProjectInfoScreen(final Project.NameKey toShow) {
     super(toShow);
-    projectName = toShow.get();
   }
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
 
+    Resources.I.style().ensureInjected();
     saveProject = new Button(Util.C.buttonSaveChanges());
     saveProject.addClickHandler(new ClickHandler() {
       @Override
@@ -75,45 +89,57 @@
       }
     });
 
-    add(new ProjectDownloadPanel(projectName, true));
+    add(new ProjectDownloadPanel(getProjectKey().get(), true));
 
     initDescription();
     grid = new LabeledWidgetsGrid();
+    actionsGrid = new LabeledWidgetsGrid();
     initProjectOptions();
     initAgreements();
     add(grid);
     add(saveProject);
+    add(actionsGrid);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.PROJECT_SVC.projectDetail(getProjectKey(),
-        new ScreenLoadCallback<ProjectDetail>(this) {
-          public void preDisplay(final ProjectDetail result) {
-            enableForm(result.canModifyAgreements,
-                result.canModifyDescription, result.canModifyMergeType, result.canModifyState);
-            saveProject.setVisible(
-                result.canModifyAgreements ||
-                result.canModifyDescription ||
-                result.canModifyMergeType ||
-                result.canModifyState);
+
+    Project.NameKey project = getProjectKey();
+    CallbackGroup cbg = new CallbackGroup();
+    AccessMap.get(project,
+        cbg.add(new GerritCallback<ProjectAccessInfo>() {
+          @Override
+          public void onSuccess(ProjectAccessInfo result) {
+            isOwner = result.isOwner();
+            enableForm();
+            saveProject.setVisible(isOwner);
+          }
+        }));
+    ProjectApi.getConfig(project,
+        cbg.addFinal(new ScreenLoadCallback<ConfigInfo>(this) {
+          @Override
+          public void preDisplay(ConfigInfo result) {
             display(result);
           }
-        });
+        }));
+
     savedPanel = INFO;
   }
 
-  private void enableForm(final boolean canModifyAgreements,
-      final boolean canModifyDescription, final boolean canModifyMergeType,
-      final boolean canModifyState) {
-    submitType.setEnabled(canModifyMergeType);
-    state.setEnabled(canModifyState);
-    contentMerge.setEnabled(canModifyMergeType);
-    descTxt.setEnabled(canModifyDescription);
-    contributorAgreements.setEnabled(canModifyAgreements);
-    signedOffBy.setEnabled(canModifyAgreements);
-    requireChangeID.setEnabled(canModifyMergeType);
+  private void enableForm() {
+    enableForm(isOwner);
+  }
+
+  private void enableForm(boolean isOwner) {
+    submitType.setEnabled(isOwner);
+    state.setEnabled(isOwner);
+    contentMerge.setEnabled(isOwner);
+    descTxt.setEnabled(isOwner);
+    contributorAgreements.setEnabled(isOwner);
+    signedOffBy.setEnabled(isOwner);
+    requireChangeID.setEnabled(isOwner);
+    maxObjectSizeLimit.setEnabled(isOwner);
   }
 
   private void initDescription() {
@@ -160,6 +186,15 @@
     requireChangeID = newInheritedBooleanBox();
     saveEnabler.listenTo(requireChangeID);
     grid.addHtml(Util.C.requireChangeID(), requireChangeID);
+
+    maxObjectSizeLimit = new NpTextBox();
+    saveEnabler.listenTo(maxObjectSizeLimit);
+    effectiveMaxObjectSizeLimit = new Label();
+    HorizontalPanel p = new HorizontalPanel();
+    p.setStyleName(Gerrit.RESOURCES.css().maxObjectSizeLimitPanel());
+    p.add(maxObjectSizeLimit);
+    p.add(effectiveMaxObjectSizeLimit);
+    grid.addHtml(Util.C.headingMaxObjectSizeLimit(), p);
   }
 
   private static ListBox newInheritedBooleanBox() {
@@ -180,9 +215,9 @@
     if (SubmitType.FAST_FORWARD_ONLY.equals(Project.SubmitType
         .valueOf(submitType.getValue(submitType.getSelectedIndex())))) {
       contentMerge.setEnabled(false);
-      final InheritedBoolean inheritedBoolean = new InheritedBoolean();
-      inheritedBoolean.setValue(InheritableBoolean.FALSE);
-      setBool(contentMerge, inheritedBoolean);
+      InheritedBooleanInfo b = InheritedBooleanInfo.create();
+      b.setConfiguredValue(InheritableBoolean.FALSE);
+      setBool(contentMerge, b);
     } else {
       contentMerge.setEnabled(submitType.isEnabled());
     }
@@ -227,18 +262,18 @@
     }
   }
 
-  private void setBool(ListBox box, InheritedBoolean inheritedBoolean) {
+  private void setBool(ListBox box, InheritedBooleanInfo inheritedBoolean) {
     int inheritedIndex = -1;
     for (int i = 0; i < box.getItemCount(); i++) {
       if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
         inheritedIndex = i;
       }
-      if (box.getValue(i).startsWith(inheritedBoolean.value.name())) {
+      if (box.getValue(i).startsWith(inheritedBoolean.configured_value().name())) {
         box.setSelectedIndex(i);
       }
     }
     if (inheritedIndex >= 0) {
-      if (project.getParent(Gerrit.getConfig().getWildProject()) == null) {
+      if (getProjectKey().equals(Gerrit.getConfig().getWildProject())) {
         if (box.getSelectedIndex() == inheritedIndex) {
           for (int i = 0; i < box.getItemCount(); i++) {
             if (box.getValue(i).equals(InheritableBoolean.FALSE.name())) {
@@ -250,7 +285,7 @@
         box.removeItem(inheritedIndex);
       } else {
         box.setItemText(inheritedIndex, InheritableBoolean.INHERIT.name() + " ("
-            + inheritedBoolean.inheritedValue + ")");
+            + inheritedBoolean.inherited_value() + ")");
       }
     }
   }
@@ -267,44 +302,70 @@
     return InheritableBoolean.INHERIT;
   }
 
-  void display(final ProjectDetail result) {
-    project = result.project;
-
-    descTxt.setText(project.getDescription());
-    setBool(contributorAgreements, result.useContributorAgreements);
-    setBool(signedOffBy, result.useSignedOffBy);
-    setBool(contentMerge, result.useContentMerge);
-    setBool(requireChangeID, result.requireChangeID);
-    setSubmitType(project.getSubmitType());
-    setState(project.getState());
+  void display(ConfigInfo result) {
+    descTxt.setText(result.description());
+    setBool(contributorAgreements, result.use_contributor_agreements());
+    setBool(signedOffBy, result.use_signed_off_by());
+    setBool(contentMerge, result.use_content_merge());
+    setBool(requireChangeID, result.require_change_id());
+    setSubmitType(result.submit_type());
+    setState(result.state());
+    maxObjectSizeLimit.setText(result.max_object_size_limit().configured_value());
+    if (result.max_object_size_limit().inherited_value() != null) {
+      effectiveMaxObjectSizeLimit.setVisible(true);
+      effectiveMaxObjectSizeLimit.setText(
+          Util.M.effectiveMaxObjectSizeLimit(result.max_object_size_limit().value()));
+      effectiveMaxObjectSizeLimit.setTitle(
+          Util.M.globalMaxObjectSizeLimit(result.max_object_size_limit().inherited_value()));
+    } else {
+      effectiveMaxObjectSizeLimit.setVisible(false);
+    }
 
     saveProject.setEnabled(false);
+    initProjectActions(result);
+  }
+
+  private void initProjectActions(ConfigInfo info) {
+    actionsGrid.clear(true);
+    actionsGrid.removeAllRows();
+
+    NativeMap<ActionInfo> actions = info.actions();
+    if (actions == null || actions.isEmpty()) {
+      return;
+    }
+    actions.copyKeysIntoChildren("id");
+    actionsGrid.addHeader(new SmallHeading(Util.C.headingProjectCommands()));
+    FlowPanel actionsPanel = new FlowPanel();
+    actionsPanel.setStyleName(Gerrit.RESOURCES.css().projectActions());
+    actionsPanel.setVisible(true);
+    actionsGrid.add(Util.C.headingCommands(), actionsPanel);
+    for (String id : actions.keySet()) {
+      actionsPanel.add(new ActionButton(getProjectKey(),
+          actions.get(id)));
+    }
   }
 
   private void doSave() {
-    project.setDescription(descTxt.getText().trim());
-    project.setUseContributorAgreements(getBool(contributorAgreements));
-    project.setUseSignedOffBy(getBool(signedOffBy));
-    project.setUseContentMerge(getBool(contentMerge));
-    project.setRequireChangeID(getBool(requireChangeID));
-    if (submitType.getSelectedIndex() >= 0) {
-      project.setSubmitType(Project.SubmitType.valueOf(submitType
-          .getValue(submitType.getSelectedIndex())));
-    }
-    if (state.getSelectedIndex() >= 0) {
-      project.setState(Project.State.valueOf(state
-          .getValue(state.getSelectedIndex())));
-    }
-
-    enableForm(false, false, false, false);
-
-    Util.PROJECT_SVC.changeProjectSettings(project,
-        new GerritCallback<ProjectDetail>() {
-          public void onSuccess(final ProjectDetail result) {
-            enableForm(result.canModifyAgreements,
-                result.canModifyDescription, result.canModifyMergeType, result.canModifyState);
+    enableForm(false);
+    saveProject.setEnabled(false);
+    ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
+        getBool(contributorAgreements), getBool(contentMerge),
+        getBool(signedOffBy), getBool(requireChangeID),
+        maxObjectSizeLimit.getText().trim(),
+        Project.SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
+        Project.State.valueOf(state.getValue(state.getSelectedIndex())),
+        new GerritCallback<ConfigInfo>() {
+          @Override
+          public void onSuccess(ConfigInfo result) {
+            enableForm();
             display(result);
           }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            enableForm();
+            super.onFailure(caught);
+          }
         });
   }
 
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 ee58420..331afee 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
@@ -29,6 +29,7 @@
 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;
@@ -64,10 +65,10 @@
   protected void onLoad() {
     super.onLoad();
     display();
-    refresh();
+    refresh(false);
   }
 
-  private void refresh() {
+  private void refresh(final boolean open) {
     setToken(subname == null || "".equals(subname) ? ADMIN_PROJECTS
         : ADMIN_PROJECTS + "?filter=" + URL.encodeQueryString(subname));
     ProjectMap.match(subname,
@@ -75,7 +76,12 @@
             new GerritCallback<ProjectMap>() {
               @Override
               public void onSuccess(ProjectMap result) {
-                projects.display(result);
+                if (open && result.values().length() > 0) {
+                  Gerrit.display(PageLinks.toProject(
+                      result.values().get(0).name_key()));
+                } else {
+                  projects.display(result);
+                }
               }
             }));
   }
@@ -153,7 +159,7 @@
       @Override
       public void onKeyUp(KeyUpEvent event) {
         subname = filterTxt.getValue();
-        refresh();
+        refresh(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
       }
     });
     hp.add(filterTxt);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
index 6ff9bff..bf04b07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
@@ -17,7 +17,6 @@
 import com.google.gwt.editor.client.IsEditor;
 import com.google.gwt.editor.client.adapters.TakesValueEditor;
 import com.google.gwt.text.shared.Renderer;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.IntegerBox;
 import com.google.gwt.user.client.ui.ValueBoxBase.TextAlignment;
@@ -56,7 +55,7 @@
 
     @Override
     void setEnabled(boolean on) {
-      DOM.setElementPropertyBoolean(list.getElement(), "disabled", !on);
+      list.getElement().setPropertyBoolean("disabled", !on);
     }
 
     @Override
@@ -77,7 +76,7 @@
 
     @Override
     void setEnabled(boolean on) {
-      DOM.setElementPropertyBoolean(box.getElement(), "disabled", !on);
+      box.getElement().setPropertyBoolean("disabled", !on);
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
new file mode 100644
index 0000000..60377c4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
@@ -0,0 +1,162 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ActionContext extends JavaScriptObject {
+  static final native void init() /*-{
+    var Gerrit = $wnd.Gerrit;
+    var doc = $wnd.document;
+    var stopPropagation = function (e) {
+      if (e && e.stopPropagation) e.stopPropagation();
+      else $wnd.event.cancelBubble = true;
+    };
+
+    Gerrit.ActionContext = function(u){this._u=u};
+    Gerrit.ActionContext.prototype = {
+      go: Gerrit.go,
+      refresh: Gerrit.refresh,
+
+      br: function(){return doc.createElement('br')},
+      hr: function(){return doc.createElement('hr')},
+      button: function(label, o) {
+        var e = doc.createElement('button');
+        e.appendChild(this.div(doc.createTextNode(label)));
+        if (o && o.onclick) e.onclick = o.onclick;
+        return e;
+      },
+      checkbox: function() {
+        var e = doc.createElement('input');
+        e.type = 'checkbox';
+        return e;
+      },
+      div: function() {
+        var e = doc.createElement('div');
+        for (var i = 0; i < arguments.length; i++)
+          e.appendChild(arguments[i]);
+        return e;
+      },
+      label: function(c,label) {
+        var e = doc.createElement('label');
+        e.appendChild(c);
+        e.appendChild(doc.createTextNode(label));
+        return e;
+      },
+      prependLabel: function(label,c) {
+        var e = doc.createElement('label');
+        e.appendChild(doc.createTextNode(label));
+        e.appendChild(c);
+        return e;
+      },
+      span: function() {
+        var e = doc.createElement('span');
+        for (var i = 0; i < arguments.length; i++)
+          e.appendChild(arguments[i]);
+        return e;
+      },
+      msg: function(label) {
+        var e = doc.createElement('span');
+        e.appendChild(doc.createTextNode(label));
+        return e;
+      },
+      textarea: function(o) {
+        var e = doc.createElement('textarea');
+        e.onkeypress = stopPropagation;
+        if (o && o.rows) e.rows = o.rows;
+        if (o && o.cols) e.cols = o.cols;
+        return e;
+      },
+      textfield: function() {
+        var e = doc.createElement('input');
+        e.type = 'text';
+        e.onkeypress = stopPropagation;
+        return e;
+      },
+
+      popup: function(e){this._p=@com.google.gerrit.client.api.PopupHelper::popup(Lcom/google/gerrit/client/api/ActionContext;Lcom/google/gwt/dom/client/Element;)(this,e)},
+      hide: function() {
+        this._p.@com.google.gerrit.client.api.PopupHelper::hide()();
+        delete this['_p'];
+      },
+
+      call: function(i,b) {
+        var m = this.action.method.toLowerCase();
+        if (m == 'get' || m == 'delete' || i==null) this[m](b);
+        else this[m](i,b);
+      },
+      get: function(b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
+      post: function(i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,i,b)},
+      put: function(i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,i,b)},
+      'delete': function(b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
+    };
+  }-*/;
+
+  static final native ActionContext create(RestApi f)/*-{
+    return new $wnd.Gerrit.ActionContext(f);
+  }-*/;
+
+  final native void set(ActionInfo a) /*-{ this.action=a; }-*/;
+  final native void set(ChangeInfo c) /*-{ this.change=c; }-*/;
+  final native void set(Project.NameKey p) /*-{ this.project=p; }-*/;
+  final native void set(RevisionInfo r) /*-{ this.revision=r; }-*/;
+
+  final native void button(ActionButton b) /*-{ this._b=b; }-*/;
+  final native ActionButton button() /*-{ return this._b; }-*/;
+
+  public final native boolean has_popup() /*-{ return this.hasOwnProperty('_p') }-*/;
+  public final native void hide() /*-{ this.hide(); }-*/;
+
+  protected ActionContext() {
+  }
+
+  static final void get(RestApi api, JavaScriptObject cb) {
+    api.get(wrap(cb));
+  }
+
+  static final void post(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
+    api.post(in, wrap(cb));
+  }
+
+  static final void put(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
+    api.put(in, wrap(cb));
+  }
+
+  static final void delete(RestApi api, JavaScriptObject cb) {
+    api.delete(wrap(cb));
+  }
+
+  private static GerritCallback<JavaScriptObject> wrap(final JavaScriptObject cb) {
+    return new GerritCallback<JavaScriptObject>() {
+      @Override
+      public void onSuccess(JavaScriptObject result) {
+        if (NativeString.is(result)) {
+          NativeString s = result.cast();
+          ApiGlue.invoke(cb, s.asString());
+        } else {
+          ApiGlue.invoke(cb, result);
+        }
+      }
+    };
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
new file mode 100644
index 0000000..f3fb1fa
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Window;
+
+public class ApiGlue {
+  private static String pluginName;
+
+  public static void init() {
+    init0();
+    ActionContext.init();
+  }
+
+  private static native void init0() /*-{
+    var serverUrl = @com.google.gwt.core.client.GWT::getHostPageBaseURL()();
+    var Plugin = function (name){this.name = name};
+    var Gerrit = {
+      getPluginName: @com.google.gerrit.client.api.ApiGlue::getPluginName(),
+      install: function (f) {
+        var p = new Plugin(this.getPluginName());
+        @com.google.gerrit.client.api.ApiGlue::install(Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(f,p);
+      },
+
+      go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
+      refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
+
+      change_actions: {},
+      revision_actions: {},
+      project_actions: {},
+      onAction: function (t,n,c){this._onAction(this.getPluginName(),t,n,c)},
+      _onAction: function (p,t,n,c) {
+        var i = p+'~'+n;
+        if ('change' == t) this.change_actions[i]=c;
+        else if ('revision' == t) this.revision_actions[i]=c;
+        else if ('project' == t) this.project_actions[i]=c;
+      },
+
+      url: function (d) {
+        if (d && d.length > 0)
+          return serverUrl + (d.charAt(0)=='/' ? d.substring(1) : d);
+        return serverUrl;
+      },
+
+      _api: function(u) {return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u)},
+      get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+      post: function(u,i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+      put: function(u,i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+      'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+    };
+
+    Plugin.prototype = {
+      getPluginName: function(){return this.name},
+      go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
+      refresh: Gerrit.refresh,
+      onAction: function(t,n,c) {Gerrit._onAction(this.name,t,n,c)},
+
+      url: function (d) {
+        var u = serverUrl + 'plugins/' + this.name + '/';
+        if (d && d.length > 0) u += d.charAt(0)=='/' ? d.substring(1) : d;
+        return u;
+      },
+
+      _api: function(d) {
+        var u = 'plugins/' + this.name + '/';
+        if (d && d.length > 0) u += d.charAt(0)=='/' ? d.substring(1) : d;
+        return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u);
+      },
+
+      get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+      post: function(u,i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+      put: function(u,i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+      'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+    };
+
+    $wnd.Gerrit = Gerrit;
+  }-*/;
+
+  private static void install(JavaScriptObject cb, JavaScriptObject p) {
+    try {
+      pluginName = PluginName.get();
+      invoke(cb, p);
+    } finally {
+      pluginName = null;
+    }
+  }
+
+  private static final String getPluginName() {
+    return pluginName != null ? pluginName : PluginName.get();
+  }
+
+  private static final void go(String urlOrToken) {
+    if (urlOrToken.startsWith("http:")
+        || urlOrToken.startsWith("https:")
+        || urlOrToken.startsWith("//")) {
+      Window.Location.assign(urlOrToken);
+    } else {
+      Gerrit.display(urlOrToken);
+    }
+  }
+
+  private static final void refresh() {
+    Gerrit.display(History.getToken());
+  }
+
+  static final native void invoke(JavaScriptObject f) /*-{ f(); }-*/;
+  static final native void invoke(JavaScriptObject f, JavaScriptObject a) /*-{ f(a); }-*/;
+  static final native void invoke(JavaScriptObject f, String a) /*-{ f(a); }-*/;
+
+  private ApiGlue() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
new file mode 100644
index 0000000..7967147
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ChangeGlue {
+  public static void onAction(
+      ChangeInfo change,
+      ActionInfo action,
+      ActionButton button) {
+    RestApi api = ChangeApi.change(change.legacy_id().get()).view(action.id());
+    JavaScriptObject f = get(action.id());
+    if (f != null) {
+      ActionContext c = ActionContext.create(api);
+      c.set(action);
+      c.set(change);
+      c.button(button);
+      ApiGlue.invoke(f, c);
+    } else {
+      DefaultActions.invoke(change, action, api);
+    }
+  }
+
+  private static final native JavaScriptObject get(String id) /*-{
+    return $wnd.Gerrit.change_actions[id];
+  }-*/;
+
+  private ChangeGlue() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
new file mode 100644
index 0000000..fcd6056
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+class DefaultActions {
+  static void invoke(ChangeInfo change, ActionInfo action, RestApi api) {
+    final Change.Id id = change.legacy_id();
+    AsyncCallback<JavaScriptObject> cb = new GerritCallback<JavaScriptObject>() {
+      @Override
+      public void onSuccess(JavaScriptObject msg) {
+        if (NativeString.is(msg)) {
+          NativeString str = (NativeString) msg;
+          if (!str.asString().isEmpty()) {
+            Window.alert(str.asString());
+          }
+        }
+        Gerrit.display(PageLinks.toChange(id));
+      }
+    };
+    invoke(action, api, cb);
+  }
+
+  static void invokeProjectAction(ActionInfo action, RestApi api) {
+    AsyncCallback<JavaScriptObject> cb = new GerritCallback<JavaScriptObject>() {
+      @Override
+      public void onSuccess(JavaScriptObject msg) {
+        if (NativeString.is(msg)) {
+          NativeString str = (NativeString) msg;
+          if (!str.asString().isEmpty()) {
+            Window.alert(str.asString());
+          }
+        }
+        Gerrit.display(PageLinks.ADMIN_PROJECTS);
+      }
+    };
+    invoke(action, api, cb);
+  }
+
+  private static void invoke(ActionInfo action, RestApi api,
+      AsyncCallback<JavaScriptObject> cb) {
+    if ("PUT".equalsIgnoreCase(action.method())) {
+      api.put(JavaScriptObject.createObject(), cb);
+    } else if ("DELETE".equalsIgnoreCase(action.method())) {
+      api.delete(cb);
+    } else {
+      api.post(JavaScriptObject.createObject(), cb);
+    }
+  }
+
+  private DefaultActions() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
new file mode 100644
index 0000000..5fc87d5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptException;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.impl.StackTraceCreator;
+
+/**
+ * Determines the name a plugin has been installed under.
+ *
+ * This implementation guesses the name a plugin runs under by looking at the
+ * JavaScript call stack and identifying the URL of the script file calling
+ * {@code Gerrit.install()}. The simple approach applied here is looking at
+ * the source URLs and extracting the name out of the string, e.g.:
+ * {@code "http://localhost:8080/plugins/{name}/static/foo.js"}.
+ */
+class PluginName {
+  private static final String UNKNOWN = "<unknown>";
+
+  static String get() {
+    return GWT.<PluginName> create(PluginName.class).guessName();
+  }
+
+  String guessName() {
+    JavaScriptException err = makeException();
+    if (hasStack(err)) {
+      return PluginNameMoz.guessName(err);
+    }
+
+    String baseUrl = baseUrl();
+    StackTraceElement[] trace = getTrace(err);
+    for (int i = trace.length - 1; i >= 0; i--) {
+      String u = trace[i].getFileName();
+      if (u != null && u.startsWith(baseUrl)) {
+        int s = u.indexOf('/', baseUrl.length());
+        if (s > 0) {
+          return u.substring(baseUrl.length(), s);
+        }
+      }
+    }
+    return UNKNOWN;
+  }
+
+  private static String baseUrl() {
+    return GWT.getHostPageBaseURL() + "plugins/";
+  }
+
+  private static StackTraceElement[] getTrace(JavaScriptException err) {
+    StackTraceCreator.fillInStackTrace(err);
+    return err.getStackTrace();
+  }
+
+  protected static final native JavaScriptException makeException()
+  /*-{ try { null.a() } catch (e) { return e } }-*/;
+
+  private static final native boolean hasStack(JavaScriptException e)
+  /*-{ return !!e.stack }-*/;
+
+  /** Extracts URL from the stack frame. */
+  static class PluginNameMoz extends PluginName {
+    String guessName() {
+      return guessName(makeException());
+    }
+
+    static String guessName(JavaScriptException e) {
+      String baseUrl = baseUrl();
+      JsArrayString stack = getStack(e);
+      for (int i = stack.length() - 1; i >= 0; i--) {
+        String frame = stack.get(i);
+        int at = frame.indexOf(baseUrl);
+        if (at >= 0) {
+          int s = frame.indexOf('/', at + baseUrl.length());
+          if (s > 0) {
+            return frame.substring(at + baseUrl.length(), s);
+          }
+        }
+      }
+      return UNKNOWN;
+    }
+
+    private static final native JsArrayString getStack(JavaScriptException e)
+    /*-{ return e.stack ? e.stack.split('\n') : [] }-*/;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java
new file mode 100644
index 0000000..95d010c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.change.Resources;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class PopupHelper {
+  static PopupHelper popup(ActionContext ctx, Element panel) {
+    PopupHelper helper = new PopupHelper(ctx.button(), panel);
+    helper.show();
+    ctx.button().link(ctx);
+    return helper;
+  }
+
+  private final ActionButton activatingButton;
+  private final FlowPanel panel;
+  private PluginSafePopupPanel popup;
+
+  PopupHelper(ActionButton button, Element child) {
+    activatingButton = button;
+    panel = new FlowPanel();
+    panel.setStyleName(Resources.I.style().popupContent());
+    panel.getElement().appendChild(child);
+  }
+
+  void show() {
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(Resources.I.style().popup());
+    p.addAutoHidePartner(activatingButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        activatingButton.unlink();
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(panel);
+    p.showRelativeTo(activatingButton);
+    GlobalKey.dialog(p);
+    popup = p;
+  }
+
+  void hide() {
+    if (popup != null) {
+      activatingButton.unlink();
+      popup.hide();
+      popup = null;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
new file mode 100644
index 0000000..b95f4e0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ProjectGlue {
+  public static void onAction(
+      Project.NameKey project,
+      ActionInfo action,
+      ActionButton button) {
+    RestApi api = ProjectApi.project(project).view(action.id());
+    JavaScriptObject f = get(action.id());
+    if (f != null) {
+      ActionContext c = ActionContext.create(api);
+      c.set(action);
+      c.set(project);
+      c.button(button);
+      ApiGlue.invoke(f, c);
+    } else {
+      DefaultActions.invokeProjectAction(action, api);
+    }
+  }
+
+  private static final native JavaScriptObject get(String id) /*-{
+    return $wnd.Gerrit.project_actions[id];
+  }-*/;
+
+  private ProjectGlue() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
new file mode 100644
index 0000000..fb489cc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class RevisionGlue {
+  public static void onAction(
+      ChangeInfo change,
+      RevisionInfo revision,
+      ActionInfo action,
+      ActionButton button) {
+    RestApi api = ChangeApi.revision(
+          change.legacy_id().get(),
+          revision.name())
+      .view(action.id());
+
+    JavaScriptObject f = get(action.id());
+    if (f != null) {
+      ActionContext c = ActionContext.create(api);
+      c.set(action);
+      c.set(change);
+      c.set(revision);
+      c.button(button);
+      ApiGlue.invoke(f, c);
+    } else {
+      DefaultActions.invoke(change, action, api);
+    }
+  }
+
+  private static final native JavaScriptObject get(String id) /*-{
+    return $wnd.Gerrit.revision_actions[id];
+  }-*/;
+
+  private RevisionGlue() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
new file mode 100644
index 0000000..60fbee4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.Button;
+
+class AbandonAction extends ActionMessageBox {
+  private final Change.Id id;
+
+  AbandonAction(Button b, Change.Id id) {
+    super(b);
+    this.id = id;
+  }
+
+  void send(String message) {
+    ChangeApi.abandon(id.get(), message, new GerritCallback<ChangeInfo>() {
+      @Override
+      public void onSuccess(ChangeInfo result) {
+        Gerrit.display(PageLinks.toChange(id));
+        hide();
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
new file mode 100644
index 0000000..45f122c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+abstract class ActionMessageBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, ActionMessageBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  static interface Style extends CssResource {
+    String popup();
+  }
+
+  private final Button activatingButton;
+  private PluginSafePopupPanel popup;
+
+  @UiField Style style;
+  @UiField NpTextArea message;
+  @UiField Button send;
+
+  ActionMessageBox(Button button) {
+    this.activatingButton = button;
+    initWidget(uiBinder.createAndBindUi(this));
+    send.setText(button.getText());
+  }
+
+  abstract void send(String message);
+
+  void show() {
+    if (popup != null) {
+      popup.hide();
+      popup = null;
+      return;
+    }
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(style.popup());
+    p.addAutoHidePartner(activatingButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(this);
+    p.showRelativeTo(activatingButton);
+    GlobalKey.dialog(p);
+    message.setFocus(true);
+    popup = p;
+  }
+
+  void hide() {
+    if (popup != null) {
+      popup.hide();
+      popup = null;
+    }
+  }
+
+  @UiHandler("message")
+  void onMessageKey(KeyPressEvent event) {
+    if ((event.getCharCode() == '\n' || event.getCharCode() == KeyCodes.KEY_ENTER)
+        && event.isControlKeyDown()) {
+      event.preventDefault();
+      event.stopPropagation();
+      onSend(null);
+    }
+  }
+
+  @UiHandler("send")
+  void onSend(ClickEvent e) {
+    send(message.getValue().trim());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
new file mode 100644
index 0000000..d639150
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style type='com.google.gerrit.client.change.ActionMessageBox.Style'>
+    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+    .popup { background-color: trimColor; }
+    .section {
+      padding: 5px 5px;
+      border-bottom: 1px solid #b8b8b8;
+    }
+  </ui:style>
+  <g:HTMLPanel>
+    <div class='{style.section}'>
+      <c:NpTextArea
+         visibleLines='3'
+         characterWidth='40'
+         ui:field='message'/>
+    </div>
+    <div class='{style.section}'>
+      <g:Button ui:field='send'
+          title='(Shortcut: Ctrl-Enter)'
+          styleName='{res.style.button}'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Send</ui:msg></div>
+      </g:Button>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
new file mode 100644
index 0000000..1cc0cff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+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.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+import java.util.TreeSet;
+
+class Actions extends Composite {
+  private static final String[] CORE = {
+    "abandon", "restore", "revert", "topic",
+    "cherrypick", "submit", "rebase", "message",
+    "publish", "/"};
+
+  interface Binder extends UiBinder<FlowPanel, Actions> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField Button cherrypick;
+  @UiField Button deleteChange;
+  @UiField Button deleteRevision;
+  @UiField Button publish;
+  @UiField Button rebase;
+  @UiField Button revert;
+  @UiField Button submit;
+
+  @UiField Button abandon;
+  private AbandonAction abandonAction;
+
+  @UiField Button restore;
+  private RestoreAction restoreAction;
+
+  private Change.Id changeId;
+  private ChangeInfo changeInfo;
+  private String revision;
+  private String project;
+  private String subject;
+  private String message;
+  private boolean canSubmit;
+
+  Actions() {
+    initWidget(uiBinder.createAndBindUi(this));
+    getElement().setId("change_actions");
+  }
+
+  void display(ChangeInfo info, String revision) {
+    this.revision = revision;
+
+    boolean hasUser = Gerrit.isSignedIn();
+    RevisionInfo revInfo = info.revision(revision);
+    CommitInfo commit = revInfo.commit();
+    changeId = info.legacy_id();
+    project = info.project();
+    subject = commit.subject();
+    message = commit.message();
+    changeInfo = info;
+
+    initChangeActions(info, hasUser);
+    initRevisionActions(info, revInfo, hasUser);
+  }
+
+  private void initChangeActions(ChangeInfo info, boolean hasUser) {
+    NativeMap<ActionInfo> actions = info.has_actions()
+        ? info.actions()
+        : NativeMap.<ActionInfo> create();
+    actions.copyKeysIntoChildren("id");
+
+    if (hasUser) {
+      a2b(actions, "/", deleteChange);
+      a2b(actions, "abandon", abandon);
+      a2b(actions, "restore", restore);
+      a2b(actions, "revert", revert);
+      for (String id : filterNonCore(actions)) {
+        add(new ActionButton(info, actions.get(id)));
+      }
+    }
+  }
+
+  private void initRevisionActions(ChangeInfo info, RevisionInfo revInfo,
+      boolean hasUser) {
+    NativeMap<ActionInfo> actions = revInfo.has_actions()
+        ? revInfo.actions()
+        : NativeMap.<ActionInfo> create();
+    actions.copyKeysIntoChildren("id");
+
+    canSubmit = false;
+    if (hasUser) {
+      canSubmit = actions.containsKey("submit");
+      if (canSubmit) {
+        submit.setTitle(actions.get("submit").title());
+      }
+      a2b(actions, "/", deleteRevision);
+      a2b(actions, "cherrypick", cherrypick);
+      a2b(actions, "publish", publish);
+      a2b(actions, "rebase", rebase);
+      for (String id : filterNonCore(actions)) {
+        add(new ActionButton(info, revInfo, actions.get(id)));
+      }
+    }
+  }
+
+  private void add(ActionButton b) {
+    ((FlowPanel) getWidget()).add(b);
+  }
+
+  private static TreeSet<String> filterNonCore(NativeMap<ActionInfo> m) {
+    TreeSet<String> ids = new TreeSet<String>(m.keySet());
+    for (String id : CORE) {
+      ids.remove(id);
+    }
+    return ids;
+  }
+
+  void setSubmitEnabled(boolean ok) {
+    submit.setVisible(ok && canSubmit);
+  }
+
+  boolean isSubmitEnabled() {
+    return submit.isVisible() && submit.isEnabled();
+  }
+
+  @UiHandler("abandon")
+  void onAbandon(ClickEvent e) {
+    if (abandonAction == null) {
+      abandonAction = new AbandonAction(abandon, changeId);
+    }
+    abandonAction.show();
+  }
+
+  @UiHandler("publish")
+  void onPublish(ClickEvent e) {
+    DraftActions.publish(changeId, revision);
+  }
+
+  @UiHandler("deleteRevision")
+  void onDeleteRevision(ClickEvent e) {
+    DraftActions.delete(changeId, revision);
+  }
+
+  @UiHandler("deleteChange")
+  void onDeleteChange(ClickEvent e) {
+    DraftActions.delete(changeId);
+  }
+
+  @UiHandler("restore")
+  void onRestore(ClickEvent e) {
+    if (restoreAction == null) {
+      restoreAction = new RestoreAction(restore, changeId);
+    }
+    restoreAction.show();
+  }
+
+  @UiHandler("rebase")
+  void onRebase(ClickEvent e) {
+    RebaseAction.call(changeId, revision);
+  }
+
+  @UiHandler("submit")
+  void onSubmit(ClickEvent e) {
+    SubmitAction.call(changeId, revision);
+  }
+
+  @UiHandler("cherrypick")
+  void onCherryPick(ClickEvent e) {
+    CherryPickAction.call(cherrypick, changeInfo, revision, project, message);
+  }
+
+  @UiHandler("revert")
+  void onRevert(ClickEvent e) {
+    RevertAction.call(cherrypick, changeId, revision, project, subject);
+  }
+
+  private static void a2b(NativeMap<ActionInfo> actions, String a, Button b) {
+    if (actions.containsKey(a)) {
+      b.setVisible(true);
+      b.setTitle(actions.get(a).title());
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
new file mode 100644
index 0000000..a4b19ff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:style>
+    #change_actions {
+      padding-top: 2px;
+      padding-bottom: 20px;
+    }
+
+    #change_actions 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;
+    }
+    #change_actions button div {
+      color: #444; 
+      height: 10px;
+      min-width: 54px;
+      line-height: 10px;
+      white-space: nowrap;
+    }
+
+    #change_actions button {
+      color: #444;
+      background-color: #f5f5f5;
+      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+    }
+    #change_actions button div {color: #444;}
+
+    #change_actions button.red {
+      color: #d14836;
+      background-color: #d14836;
+      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
+    }
+    #change_actions button.red div {color: #fff;}
+
+    #change_actions button.submit {
+      float: right;
+      background-color: #4d90fe;
+      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+    }
+    #change_actions button.submit div {color: #fff;}
+
+    #change_actions button:disabled {
+      font-weight: normal;
+      background-color: #999;
+      background-image: -webkit-linear-gradient(top, #999, #999);
+    }
+
+  </ui:style>
+
+  <g:FlowPanel>
+    <g:Button ui:field='cherrypick' styleName='' visible='false'>
+      <div><ui:msg>Cherry Pick</ui:msg></div>
+    </g:Button>
+    <g:Button ui:field='rebase' styleName='' visible='false'>
+      <div><ui:msg>Rebase</ui:msg></div>
+    </g:Button>
+    <g:Button ui:field='revert' styleName='' visible='false'>
+      <div><ui:msg>Revert</ui:msg></div>
+    </g:Button>
+    <g:Button ui:field='deleteChange' styleName='' visible='false'>
+      <div><ui:msg>Delete Change</ui:msg></div>
+    </g:Button>
+    <g:Button ui:field='deleteRevision' styleName='' visible='false'>
+      <div><ui:msg>Delete Revision</ui:msg></div>
+    </g:Button>
+    <g:Button ui:field='publish' styleName='' visible='false'>
+      <div><ui:msg>Publish</ui:msg></div>
+    </g:Button>
+
+    <g:Button ui:field='abandon' styleName='{style.red}' visible='false'>
+      <div><ui:msg>Abandon</ui:msg></div>
+    </g:Button>
+    <g:Button ui:field='restore' styleName='{style.red}' visible='false'>
+      <div><ui:msg>Restore</ui:msg></div>
+    </g:Button>
+
+    <g:Button ui:field='submit' styleName='{style.submit}' visible='false'>
+      <div><ui:msg>Submit</ui:msg></div>
+    </g:Button>
+  </g:FlowPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
new file mode 100644
index 0000000..a1c7a0c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -0,0 +1,781 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.MergeableInfo;
+import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.RevisionInfoCache;
+import com.google.gerrit.client.changes.StarredChanges;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.diff.DiffApi;
+import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.BranchLink;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.client.ui.UserActivityMonitor;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.DOM;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+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.ToggleButton;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+public class ChangeScreen2 extends Screen {
+  interface Binder extends UiBinder<HTMLPanel, ChangeScreen2> {}
+  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 label_may();
+    String label_need();
+    String replyBox();
+    String selected();
+  }
+
+  static ChangeScreen2 get(NativeEvent in) {
+    com.google.gwt.user.client.Element e = in.getEventTarget().cast();
+    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+      EventListener l = DOM.getEventListener(e);
+      if (l instanceof ChangeScreen2) {
+        return (ChangeScreen2) l;
+      }
+    }
+    return null;
+  }
+
+  private final Change.Id changeId;
+  private String revision;
+  private ChangeInfo changeInfo;
+  private CommentLinkProcessor commentLinkProcessor;
+
+  private KeyCommandSet keysNavigation;
+  private KeyCommandSet keysAction;
+  private List<HandlerRegistration> handlers = new ArrayList<HandlerRegistration>(4);
+  private UpdateCheckTimer updateCheck;
+  private Timestamp lastDisplayedUpdate;
+  private UpdateAvailableBar updateAvailable;
+  private boolean openReplyBox;
+
+  @UiField HTMLPanel headerLine;
+  @UiField Style style;
+  @UiField ToggleButton star;
+  @UiField Reload reload;
+  @UiField AnchorElement permalink;
+
+  @UiField Element reviewersText;
+  @UiField Reviewers reviewers;
+  @UiField Element changeIdText;
+  @UiField Element ownerText;
+  @UiField Element statusText;
+  @UiField Image projectQuery;
+  @UiField InlineHyperlink projectLink;
+  @UiField InlineHyperlink branchLink;
+  @UiField Element submitActionText;
+  @UiField Element notMergeable;
+  @UiField CopyableLabel idText;
+  @UiField Topic topic;
+  @UiField Element actionText;
+  @UiField Element actionDate;
+
+  @UiField Actions actions;
+  @UiField Labels labels;
+  @UiField CommitBox commit;
+  @UiField RelatedChanges related;
+  @UiField FileTable files;
+  @UiField FlowPanel history;
+
+  @UiField Button includedIn;
+  @UiField Button revisions;
+  @UiField Button download;
+  @UiField Button reply;
+  @UiField Button expandAll;
+  @UiField Button collapseAll;
+  @UiField Button editMessage;
+  @UiField QuickApprove quickApprove;
+  private ReplyAction replyAction;
+  private EditMessageAction editMessageAction;
+  private IncludedInAction includedInAction;
+  private RevisionsAction revisionsAction;
+  private DownloadAction downloadAction;
+
+  public ChangeScreen2(Change.Id changeId, String revision, boolean openReplyBox) {
+    this.changeId = changeId;
+    this.revision = revision != null && !revision.isEmpty() ? revision : null;
+    this.openReplyBox = openReplyBox;
+    add(uiBinder.createAndBindUi(this));
+  }
+
+  Change.Id getChangeId() {
+    return changeId;
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    loadChangeInfo(true, new GerritCallback<ChangeInfo>() {
+      @Override
+      public void onSuccess(ChangeInfo info) {
+        info.init();
+        loadConfigInfo(info);
+      }
+    });
+  }
+
+  void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
+    RestApi call = ChangeApi.detail(changeId.get());
+    ChangeList.addOptions(call, EnumSet.of(
+      ListChangesOption.CURRENT_ACTIONS,
+      fg && revision != null
+        ? ListChangesOption.ALL_REVISIONS
+        : ListChangesOption.CURRENT_REVISION));
+    if (!fg) {
+      call.background();
+    }
+    call.get(cb);
+  }
+
+  @Override
+  protected void onUnload() {
+    if (updateCheck != null) {
+      updateCheck.cancel();
+      updateCheck = null;
+    }
+    for (HandlerRegistration h : handlers) {
+      h.removeHandler();
+    }
+    handlers.clear();
+    super.onUnload();
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setHeaderVisible(false);
+    Resources.I.style().ensureInjected();
+    star.setVisible(Gerrit.isSignedIn());
+    labels.init(style, statusText);
+    reviewers.init(style, reviewersText);
+
+    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
+    keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
+      @Override
+      public void onKeyPress(final KeyPressEvent event) {
+        Gerrit.displayLastChangeList();
+      }
+    });
+    keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
+      @Override
+      public void onKeyPress(final KeyPressEvent event) {
+        reload.reload();
+      }
+    });
+
+    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+    keysAction.add(new KeyCommand(0, 'a', Util.C.keyPublishComments()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (Gerrit.isSignedIn()) {
+          onReply(null);
+        } else {
+          Gerrit.doSignIn(getToken());
+        }
+      }
+    });
+    if (Gerrit.isSignedIn()) {
+      keysAction.add(new KeyCommand(0, 's', Util.C.changeTableStar()) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          star.setValue(!star.getValue(), true);
+        }
+      });
+      keysAction.add(new KeyCommand(0, 'c', Util.C.keyAddReviewers()) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          reviewers.onOpenForm();
+        }
+      });
+    }
+  }
+
+  private void initIncludedInAction(ChangeInfo info) {
+    if (info.status() == Status.MERGED) {
+      includedInAction = new IncludedInAction(
+          info.legacy_id(),
+          style, headerLine, includedIn);
+      includedIn.setVisible(true);
+    }
+  }
+
+  private void initRevisionsAction(ChangeInfo info, String revision) {
+    revisionsAction = new RevisionsAction(
+        info.legacy_id(), revision,
+        style, headerLine, revisions);
+  }
+
+  private void initDownloadAction(ChangeInfo info, String revision) {
+    downloadAction =
+        new DownloadAction(info, revision, style, headerLine, download);
+  }
+
+  private void initProjectLinks(final ChangeInfo info) {
+    projectQuery.addDomHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        Gerrit.display(
+            PageLinks.toProjectDefaultDashboard(info.project_name_key()));
+      }
+    }, ClickEvent.getType());
+    projectLink.setText(info.project());
+    projectLink.setTargetHistoryToken(
+        PageLinks.toProject(info.project_name_key()));
+  }
+
+  private void initBranchLink(ChangeInfo info) {
+    branchLink.setText(info.branch());
+    branchLink.setTargetHistoryToken(
+        PageLinks.toChangeQuery(
+            BranchLink.query(
+                info.project_name_key(),
+                info.status(),
+                info.branch(),
+                info.topic())));
+  }
+
+  private void initEditMessageAction(ChangeInfo info, String revision) {
+    NativeMap<ActionInfo> actions = info.revision(revision).actions();
+    if (actions != null && actions.containsKey("message")) {
+      editMessage.setVisible(true);
+      editMessageAction = new EditMessageAction(
+          info.legacy_id(),
+          revision,
+          info.revision(revision).commit().message(),
+          style,
+          editMessage,
+          reply);
+      keysAction.add(new KeyCommand(0, 'e', Util.C.keyEditMessage()) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          editMessageAction.onEdit();
+        }
+      });
+    }
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+    handlers.add(GlobalKey.add(this, keysNavigation));
+    handlers.add(GlobalKey.add(this, keysAction));
+    files.registerKeys();
+    related.registerKeys();
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+
+    related.setMaxHeight(commit.getElement()
+        .getParentElement()
+        .getOffsetHeight());
+
+    if (openReplyBox) {
+      onReply();
+    } else {
+      String prior = Gerrit.getPriorView();
+      if (prior != null && prior.startsWith("/c/")) {
+        scrollToPath(prior.substring(3));
+      }
+    }
+
+    startPoller();
+  }
+
+  private void scrollToPath(String token) {
+    int s = token.indexOf('/');
+    try {
+      if (s < 0 || !changeId.equals(Change.Id.parse(token.substring(0, s)))) {
+        return; // Unrelated URL, do not scroll.
+      }
+    } catch (IllegalArgumentException e) {
+      return;
+    }
+
+    s = token.indexOf('/', s + 1);
+    if (s < 0) {
+      return; // URL does not name a file.
+    }
+
+    int c = token.lastIndexOf(',');
+    if (0 <= c) {
+      token = token.substring(s + 1, c);
+    } else {
+      token = token.substring(s + 1);
+    }
+
+    if (!token.isEmpty()) {
+      files.scrollToPath(KeyUtil.decode(token));
+    }
+  }
+
+  @UiHandler("star")
+  void onToggleStar(ValueChangeEvent<Boolean> e) {
+    StarredChanges.toggleStar(changeId, e.getValue());
+  }
+
+  @UiHandler("includedIn")
+  void onIncludedIn(ClickEvent e) {
+    includedInAction.show();
+  }
+
+  @UiHandler("download")
+  void onDownload(ClickEvent e) {
+    downloadAction.show();
+  }
+
+  @UiHandler("revisions")
+  void onRevision(ClickEvent e) {
+    revisionsAction.show();
+  }
+
+  @UiHandler("reply")
+  void onReply(ClickEvent e) {
+    onReply();
+  }
+
+  private void onReply() {
+    if (Gerrit.isSignedIn()) {
+      replyAction.onReply();
+    } else {
+      Gerrit.doSignIn(getToken());
+    }
+  }
+
+  @UiHandler("editMessage")
+  void onEditMessage(ClickEvent e) {
+    editMessageAction.onEdit();
+  }
+
+  @UiHandler("expandAll")
+  void onExpandAll(ClickEvent e) {
+    int n = history.getWidgetCount();
+    for (int i = 0; i < n; i++) {
+      ((Message) history.getWidget(i)).setOpen(true);
+    }
+    expandAll.setVisible(false);
+    collapseAll.setVisible(true);
+  }
+
+  @UiHandler("collapseAll")
+  void onCollapseAll(ClickEvent e) {
+    int n = history.getWidgetCount();
+    for (int i = 0; i < n; i++) {
+      ((Message) history.getWidget(i)).setOpen(false);
+    }
+    expandAll.setVisible(true);
+    collapseAll.setVisible(false);
+  }
+
+  private void loadConfigInfo(final ChangeInfo info) {
+    info.revisions().copyKeysIntoChildren("name");
+    final RevisionInfo rev = resolveRevisionToDisplay(info);
+
+    CallbackGroup group = new CallbackGroup();
+    loadDiff(rev, myLastReply(info), group);
+    loadCommit(rev, group);
+    RevisionInfoCache.add(changeId, rev);
+    ConfigInfoCache.add(info);
+    ConfigInfoCache.get(info.project_name_key(),
+      group.add(new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
+        @Override
+        protected void preDisplay(Entry result) {
+          commentLinkProcessor = result.getCommentLinkProcessor();
+          setTheme(result.getTheme());
+          renderChangeInfo(info);
+        }
+      }));
+    group.done();
+  }
+
+  private static Timestamp myLastReply(ChangeInfo info) {
+    if (Gerrit.isSignedIn() && info.messages() != null) {
+      int self = Gerrit.getUserAccountInfo()._account_id();
+      for (int i = info.messages().length() - 1; i >= 0; i--) {
+        MessageInfo m = info.messages().get(i);
+        if (m.author() != null && m.author()._account_id() == self) {
+          return m.date();
+        }
+      }
+    }
+    return null;
+  }
+
+  private void loadDiff(final RevisionInfo rev, final Timestamp myLastReply,
+      CallbackGroup group) {
+    final List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
+    final List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
+    DiffApi.list(changeId.get(),
+      rev.name(),
+      group.add(new AsyncCallback<NativeMap<FileInfo>>() {
+        @Override
+        public void onSuccess(NativeMap<FileInfo> m) {
+          files.setRevisions(null, new PatchSet.Id(changeId, rev._number()));
+          files.setValue(m, myLastReply, comments.get(0), drafts.get(0));
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      }));
+
+    if (Gerrit.isSignedIn()) {
+      ChangeApi.revision(changeId.get(), rev.name())
+        .view("files")
+        .addParameterTrue("reviewed")
+        .get(group.add(new AsyncCallback<JsArrayString>() {
+            @Override
+            public void onSuccess(JsArrayString result) {
+              files.markReviewed(result);
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          }));
+    }
+  }
+
+  private List<NativeMap<JsArray<CommentInfo>>> loadComments(
+      RevisionInfo rev, CallbackGroup group) {
+    final List<NativeMap<JsArray<CommentInfo>>> r =
+        new ArrayList<NativeMap<JsArray<CommentInfo>>>(1);
+    ChangeApi.revision(changeId.get(), rev.name())
+      .view("comments")
+      .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
+        @Override
+        public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+          r.add(result);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      }));
+    return r;
+  }
+
+  private List<NativeMap<JsArray<CommentInfo>>> loadDrafts(
+      RevisionInfo rev, CallbackGroup group) {
+    final List<NativeMap<JsArray<CommentInfo>>> r =
+        new ArrayList<NativeMap<JsArray<CommentInfo>>>(1);
+    if (Gerrit.isSignedIn()) {
+      ChangeApi.revision(changeId.get(), rev.name())
+        .view("drafts")
+        .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
+          @Override
+          public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+            r.add(result);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        }));
+    } else {
+      r.add(NativeMap.<JsArray<CommentInfo>> create());
+    }
+    return r;
+  }
+
+  private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
+    ChangeApi.revision(changeId.get(), rev.name())
+      .view("commit")
+      .get(group.add(new AsyncCallback<CommitInfo>() {
+        @Override
+        public void onSuccess(CommitInfo info) {
+          rev.set_commit(info);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      }));
+  }
+
+  private void loadMergeable(final Change.Status status, final boolean canSubmit) {
+    if (Gerrit.getConfig().testChangeMerge()) {
+      ChangeApi.revision(changeId.get(), revision)
+        .view("mergeable")
+        .get(new AsyncCallback<MergeableInfo>() {
+          @Override
+          public void onSuccess(MergeableInfo result) {
+            if (canSubmit) {
+              actions.setSubmitEnabled(result.mergeable());
+              if (status == Change.Status.NEW) {
+                statusText.setInnerText(result.mergeable()
+                    ? Util.C.readyToSubmit()
+                    : Util.C.mergeConflict());
+              }
+            }
+            setVisible(notMergeable, !result.mergeable());
+            renderSubmitType(result.submit_type());
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            loadSubmitType(status, canSubmit);
+          }
+        });
+    } else {
+      loadSubmitType(status, canSubmit);
+    }
+  }
+
+  private void loadSubmitType(final Change.Status status, final boolean canSubmit) {
+    if (canSubmit) {
+      actions.setSubmitEnabled(true);
+      if (status == Change.Status.NEW) {
+        statusText.setInnerText(Util.C.readyToSubmit());
+      }
+    }
+    ChangeApi.revision(changeId.get(), revision)
+      .view("submit_type")
+      .get(new AsyncCallback<NativeString>() {
+        @Override
+        public void onSuccess(NativeString result) {
+          renderSubmitType(result.asString());
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      });
+  }
+
+  private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
+    if (revision == null) {
+      revision = info.current_revision();
+    } else if (!info.revisions().containsKey(revision)) {
+      JsArray<RevisionInfo> list = info.revisions().values();
+      for (int i = 0; i < list.length(); i++) {
+        RevisionInfo r = list.get(i);
+        if (revision.equals(String.valueOf(r._number()))) {
+          revision = r.name();
+          break;
+        }
+      }
+    }
+    return info.revision(revision);
+  }
+
+  private void renderChangeInfo(ChangeInfo info) {
+    changeInfo = info;
+    lastDisplayedUpdate = info.updated();
+    boolean current = info.status().isOpen()
+        && revision.equals(info.current_revision());
+    boolean canSubmit = labels.set(info, current);
+
+    if (!current && info.status() == Change.Status.NEW) {
+      statusText.setInnerText(Util.C.notCurrent());
+    } else {
+      statusText.setInnerText(Util.toLongString(info.status()));
+    }
+
+    renderOwner(info);
+    renderActionTextDate(info);
+    renderHistory(info);
+    initIncludedInAction(info);
+    initRevisionsAction(info, revision);
+    initDownloadAction(info, revision);
+    initProjectLinks(info);
+    initBranchLink(info);
+    actions.display(info, revision);
+
+    star.setValue(info.starred());
+    permalink.setHref(ChangeLink.permalink(changeId));
+    changeIdText.setInnerText(String.valueOf(info.legacy_id()));
+    idText.setText("Change-Id: " + info.change_id());
+    idText.setPreviewText(info.change_id());
+    reload.set(info);
+    topic.set(info, revision);
+    commit.set(commentLinkProcessor, info, revision);
+    related.set(info, revision);
+    reviewers.set(info);
+    quickApprove.set(info, revision);
+
+    if (Gerrit.isSignedIn()) {
+      initEditMessageAction(info, revision);
+      replyAction = new ReplyAction(info, revision, style, reply);
+      if (topic.canEdit()) {
+        keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
+          @Override
+          public void onKeyPress(KeyPressEvent event) {
+            topic.onEdit();
+          }
+        });
+      }
+    }
+    if (current) {
+      loadMergeable(info.status(), canSubmit);
+    }
+
+    StringBuilder sb = new StringBuilder();
+    sb.append(Util.M.changeScreenTitleId(info.id_abbreviated()));
+    if (info.subject() != null) {
+      sb.append(": ");
+      sb.append(info.subject());
+    }
+    setWindowTitle(sb.toString());
+  }
+
+  private void renderOwner(ChangeInfo info) {
+    // TODO info card hover
+    String name = info.owner().name() != null
+        ? info.owner().name()
+        : Gerrit.getConfig().getAnonymousCowardName();
+    ownerText.setInnerText(name);
+    ownerText.setTitle(name);
+  }
+
+  private void renderSubmitType(String action) {
+    try {
+      SubmitType type = Project.SubmitType.valueOf(action);
+      submitActionText.setInnerText(
+          com.google.gerrit.client.admin.Util.toLongString(type));
+    } catch (IllegalArgumentException e) {
+      submitActionText.setInnerText(action);
+    }
+  }
+
+  private void renderActionTextDate(ChangeInfo info) {
+    String action;
+    if (info.created().equals(info.updated())) {
+      action = Util.C.changeInfoBlockUploaded();
+    } else {
+      action = Util.C.changeInfoBlockUpdated();
+    }
+    actionText.setInnerText(action);
+    actionDate.setInnerText(FormatUtil.relativeFormat(info.updated()));
+  }
+
+  private void renderHistory(ChangeInfo info) {
+    JsArray<MessageInfo> messages = info.messages();
+    if (messages != null) {
+      for (int i = 0; i < messages.length(); i++) {
+        history.add(new Message(commentLinkProcessor, messages.get(i)));
+      }
+    }
+  }
+
+  void showUpdates(ChangeInfo newInfo) {
+    if (!isAttached() || newInfo.updated().equals(lastDisplayedUpdate)) {
+      return;
+    }
+
+    JsArray<MessageInfo> om = changeInfo.messages();
+    JsArray<MessageInfo> nm = newInfo.messages();
+
+    if (om == null) {
+      om = JsArray.createArray().cast();
+    }
+    if (nm == null) {
+      nm = JsArray.createArray().cast();
+    }
+
+    if (updateAvailable == null) {
+      updateAvailable = new UpdateAvailableBar() {
+        @Override
+        void onShow() {
+          reload.reload();
+        }
+
+        void onIgnore(Timestamp newTime) {
+          lastDisplayedUpdate = newTime;
+        }
+      };
+    }
+    updateAvailable.set(
+        Natives.asList(nm).subList(om.length(), nm.length()),
+        newInfo.updated());
+    if (!updateAvailable.isAttached()) {
+      add(updateAvailable);
+    }
+  }
+
+  private void startPoller() {
+    if (Gerrit.isSignedIn() && 0 < Gerrit.getConfig().getChangeUpdateDelay()) {
+      updateCheck = new UpdateCheckTimer(this);
+      updateCheck.schedule();
+      handlers.add(UserActivityMonitor.addValueChangeHandler(updateCheck));
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
new file mode 100644
index 0000000..9f9bdce
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
@@ -0,0 +1,412 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gerrit.client.change'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:x='urn:import:com.google.gerrit.client.ui'
+    xmlns:clippy='urn:import:com.google.gwtexpui.clippy.client'>
+  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style type='com.google.gerrit.client.change.ChangeScreen2.Style'>
+    @eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
+    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+    @def INFO_WIDTH 450px;
+    @def HEADER_HEIGHT 29px;
+
+    .headerLine {
+      position: relative;
+      background-color: trimColor;
+      height: HEADER_HEIGHT;
+    }
+
+    .idBlock {
+      position: relative;
+      width: INFO_WIDTH;
+      height: HEADER_HEIGHT;
+      background-color: trimColor;
+      color: textColor;
+      font-family: sans-serif;
+      font-weight: bold;
+    }
+    .star {
+      cursor: pointer;
+      outline: none;
+      position: absolute;
+      left: 5px;
+      top: 5px;
+    }
+    .idLine, .idStatus {
+      line-height: HEADER_HEIGHT;
+    }
+    .idLine {
+      position: absolute;
+      top: 0;
+      left: 29px;
+      width: 245px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    .idStatus {
+      position: absolute;
+      top: 0;
+      right: 26px;
+    }
+    .reload {
+      display: block;
+      position: absolute;
+      top: 7px;
+      right: 5px;
+      cursor: pointer;
+    }
+
+    .headerButtons {
+      position: absolute;
+      top: 0;
+      left: INFO_WIDTH;
+      height: HEADER_HEIGHT;
+      padding-left: 5px;
+    }
+
+    .popdown {
+      position: absolute;
+      top: 2px;
+      right: 0;
+    }
+    .popdown button {
+      cursor: pointer;
+      height: 25px;
+      border: none;
+      border-left: 2px solid #fff;
+      background-color: trimColor;
+      margin: 0;
+      padding-left: 2px;
+      padding-right: 2px;
+      min-width: 100px;
+    }
+    .popdown button div {
+      padding-left: 6px;
+      padding-right: 6px;
+    }
+    .popdown button div:after {
+      content: " \25bc";
+    }
+    .popdown button.selected {
+      font-weight: bold;
+    }
+    .headerLine button:disabled,
+    .headerTable button:disabled,
+    .popdown button:disabled {
+      background-color: #999;
+      background-image: -webkit-linear-gradient(top, #999, #999);
+    }
+
+    .headerTable {
+      border-spacing: 0;
+    }
+
+    .headerTable th {
+      width: 60px;
+      color: #444;
+      font-weight: normal;
+      vertical-align: top;
+      text-align: left;
+      padding-right: 5px;
+    }
+
+    .clippy div {
+      float: right;
+    }
+
+    .queryProject {
+      float: left;
+      cursor: pointer;
+    }
+
+    .infoColumn {
+      width: 440px;
+      padding-right: 10px;
+      vertical-align: top;
+    }
+
+    #change_infoTable {
+      border-spacing: 0;
+      width: 100%;
+      margin-left: 2px;
+      margin-right: 5px;
+    }
+
+    .notMergeable {
+      float: right;
+      font-weight: bold;
+      color: red;
+    }
+
+    .commitColumn, .related {
+      padding: 0;
+      vertical-align: top;
+    }
+    .commitColumn {
+      width: 600px;
+    }
+
+    .labels {
+      border-spacing: 0;
+      padding: 0;
+    }
+    .labelName {
+      color: #444;
+      vertical-align: top;
+      text-align: left;
+      padding-top: 3px;
+      padding-right: 5px;
+      white-space: nowrap;
+    }
+
+    .label_user {
+      display: inline-block;
+      margin-bottom: 2px;
+      padding: 1px 3px 0px 3px;
+      border-radius: 5px;
+      -webkit-border-radius: 5px;
+      background: trimColor;
+      border: 1px solid trimColor;
+      white-space: nowrap;
+    }
+    .label_user img.avatar {
+      margin: 0 2px 0 0;
+      width: 16px;
+      height: 16px;
+      vertical-align: bottom;
+    }
+    .label_user button {
+      cursor: pointer;
+      padding: 0;
+      margin: 0 0 0 5px;
+      border: 0;
+      background-color: transparent;
+      white-space: nowrap;
+    }
+
+    .label_ok {color: #060;}
+    .label_reject {color: #d14836;}
+    .label_need {color: #000;}
+    .label_may {color: #777;}
+
+    .headerButtons 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;
+    }
+    .headerButtons button div {
+      color: #444;
+      height: 10px;
+      min-width: 54px;
+      line-height: 10px;
+      white-space: nowrap;
+    }
+    button.quickApprove {
+      background-color: #4d90fe;
+      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+    }
+    button.quickApprove div { color: #fff; }
+
+    .sectionHeader {
+      position: relative;
+      background-color: trimColor;
+      font-weight: bold;
+      color: textColor;
+      height: 18px;
+      padding: 5px 10px;
+    }
+    .sectionHeader .headerButtons {
+      top: 2px;
+      height: 18px;
+      border-left: 1px inset #fff;
+      padding-top: 3px;
+      padding-bottom: 3px;
+    }
+    .sectionHeader button { margin-top: 0; }
+
+    .replyBox {
+      background-color: trimColor;
+    }
+  </ui:style>
+
+  <g:HTMLPanel>
+    <g:HTMLPanel styleName='{style.headerLine}' ui:field='headerLine'>
+      <div class='{style.idBlock}'>
+        <c:StarIcon ui:field='star' styleName='{style.star}'/>
+        <div class='{style.idLine}'>
+          <ui:msg>Change <span ui:field='changeIdText'/> by <span ui:field='ownerText'/></ui:msg>
+        </div>
+        <div ui:field='statusText' class='{style.idStatus}'/>
+        <a ui:field='permalink' class='{style.reload}'>
+          <c:Reload ui:field='reload'
+              title='Reload the change (Shortcut: R)'>
+            <ui:attribute name='title'/>
+          </c:Reload>
+        </a>
+      </div>
+      <div class='{style.headerButtons}'>
+        <g:Button ui:field='reply'
+            styleName=''
+            title='Reply and score (Shortcut: a)'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Reply&#8230;</ui:msg></div>
+        </g:Button>
+        <c:QuickApprove ui:field='quickApprove'
+            styleName='{style.quickApprove}'
+            title='Apply score with one click'>
+          <ui:attribute name='title'/>
+        </c:QuickApprove>
+        <g:Button ui:field='editMessage'
+            styleName=''
+            visible='false'
+            title='Edit commit message (Shortcut: e)'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Edit Message</ui:msg></div>
+        </g:Button>
+      </div>
+
+      <g:FlowPanel styleName='{style.popdown}'>
+        <g:Button ui:field='includedIn' styleName='' visible="false">
+          <div><ui:msg>Included in</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='revisions' styleName=''>
+          <div><ui:msg>Revisions</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='download' styleName=''>
+          <div><ui:msg>Download</ui:msg></div>
+        </g:Button>
+      </g:FlowPanel>
+    </g:HTMLPanel>
+
+    <table class='{style.headerTable}'>
+      <tr>
+        <td class='{style.infoColumn}'>
+          <table id='change_infoTable'>
+            <tr>
+              <th><ui:msg>Reviewers</ui:msg></th>
+              <td ui:field='reviewersText'/>
+            </tr>
+            <tr>
+              <th><ui:msg>CC</ui:msg></th>
+              <td>
+                <c:Reviewers ui:field='reviewers'/>
+              </td>
+            </tr>
+            <tr>
+              <th><ui:msg>Project</ui:msg></th>
+              <td><g:Image
+                     ui:field='projectQuery'
+                     resource='{ico.queryIcon}'
+                     styleName='{style.queryProject}'
+                     title='Search for changes on this project'>
+                    <ui:attribute name='title'/>
+                  </g:Image>
+                  <x:InlineHyperlink ui:field='projectLink'
+                     title='Go to project'>
+                     <ui:attribute name='title'/>
+                  </x:InlineHyperlink>
+              </td>
+            </tr>
+            <tr>
+              <th><ui:msg>Branch</ui:msg></th>
+              <td><x:InlineHyperlink ui:field='branchLink'
+                     title='Search for changes on this branch'>
+                     <ui:attribute name='title'/>
+                  </x:InlineHyperlink>
+              </td>
+            </tr>
+            <tr>
+              <th><ui:msg>Strategy</ui:msg></th>
+              <td>
+                <span ui:field='submitActionText'/>
+                <div ui:field='notMergeable'
+                     class='{style.notMergeable}'
+                     style='display: none'
+                     aria-hidden='true'>
+                  <ui:msg>Cannot Merge</ui:msg>
+                </div>
+              </td>
+            </tr>
+            <tr><td colspan='2'><c:Actions ui:field='actions'/></td></tr>
+            <tr>
+              <th ui:field='actionText'/>
+              <td ui:field='actionDate'/>
+            </tr>
+            <tr>
+              <th><ui:msg>Change-Id</ui:msg></th>
+              <td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='idText'/></td>
+            </tr>
+            <tr>
+              <th><ui:msg>Topic</ui:msg></th>
+              <td><c:Topic ui:field='topic'/></td>
+            </tr>
+          </table>
+          <hr/>
+          <c:Labels ui:field='labels' styleName='{style.labels}'/>
+        </td>
+
+        <td class='{style.commitColumn}'>
+          <c:CommitBox ui:field='commit'/>
+        </td>
+
+        <td class='{style.related}'>
+          <c:RelatedChanges ui:field='related'/>
+        </td>
+      </tr>
+    </table>
+
+    <div class='{style.sectionHeader}'>
+      <ui:msg>Files</ui:msg>
+    </div>
+    <c:FileTable ui:field='files'/>
+
+    <div class='{style.sectionHeader}'>
+      <ui:msg>History</ui:msg>
+      <div class='{style.headerButtons}'>
+        <g:Button ui:field='expandAll'
+            styleName=''
+            title='Expand all messages in the change history'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Expand All</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='collapseAll'
+            styleName=''
+            visible='false'
+            title='Collapse all messages in the change history'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Collapse All</ui:msg></div>
+        </g:Button>
+      </div>
+    </div>
+    <g:FlowPanel ui:field='history'/>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
new file mode 100644
index 0000000..0dc155e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CherryPickDialog;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.user.client.ui.Button;
+
+class CherryPickAction {
+  static void call(Button b, final ChangeInfo info, final String revision,
+      String project, final String commitMessage) {
+    // TODO Replace CherryPickDialog with a nicer looking display.
+    b.setEnabled(false);
+    new CherryPickDialog(b, new Project.NameKey(project)) {
+      {
+        sendButton.setText(Util.C.buttonCherryPickChangeSend());
+        if (info.status().isClosed()) {
+          message.setText(Util.M.cherryPickedChangeDefaultMessage(
+              commitMessage.trim(),
+              revision));
+        } else {
+          message.setText(commitMessage.trim());
+        }
+      }
+
+      @Override
+      public void onSend() {
+        ChangeApi.cherrypick(info.legacy_id().get(), revision,
+            getDestinationBranch(),
+            getMessageText(),
+            new GerritCallback<ChangeInfo>() {
+              @Override
+              public void onSuccess(ChangeInfo result) {
+                sent = true;
+                hide();
+                Gerrit.display(PageLinks.toChange(result.legacy_id()));
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+                enableButtons(true);
+                super.onFailure(caught);
+              }
+            });
+      }
+    }.center();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
new file mode 100644
index 0000000..5cb0443
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class CommitBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, CommitBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField Element commitName;
+  @UiField AnchorElement browserLink;
+  @UiField InlineHyperlink authorNameEmail;
+  @UiField Element authorDate;
+  @UiField InlineHyperlink committerNameEmail;
+  @UiField Element committerDate;
+  @UiField Element commitMessageText;
+
+  CommitBox() {
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void set(CommentLinkProcessor commentLinkProcessor,
+      ChangeInfo change,
+      String revision) {
+    RevisionInfo revInfo = change.revision(revision);
+    CommitInfo commit = revInfo.commit();
+
+    commitName.setInnerText(revision);
+    formatLink(commit.author(), authorNameEmail,
+        authorDate, change.status());
+    formatLink(commit.committer(), committerNameEmail,
+        committerDate, change.status());
+    commitMessageText.setInnerSafeHtml(commentLinkProcessor.apply(
+        new SafeHtmlBuilder().append(commit.message()).linkify()));
+
+    GitwebLink gw = Gerrit.getGitwebLink();
+    if (gw != null && gw.canLink(revInfo)) {
+      browserLink.setInnerText(gw.getLinkName());
+      browserLink.setHref(gw.toRevision(change.project(), revision));
+    } else {
+      UIObject.setVisible(browserLink, false);
+    }
+  }
+
+  private static void formatLink(GitPerson person, InlineHyperlink name,
+      Element date, Status status) {
+    name.setText(renderName(person));
+    name.setTargetHistoryToken(PageLinks
+        .toAccountQuery(owner(person), status));
+    date.setInnerText(FormatUtil.mediumFormat(person.date()));
+  }
+
+  private static String renderName(GitPerson person) {
+    return person.name() + " <" + person.email() + ">";
+  }
+
+  public static String owner(GitPerson person) {
+    if (person.email() != null) {
+      return person.email();
+    } else if (person.name() != null) {
+      return person.name();
+    } else {
+      return "";
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
new file mode 100644
index 0000000..a095926
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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>
+    .commitHeader {
+      border-spacing: 0;
+      padding: 0;
+      width: 564px;
+    }
+
+    .commitHeader th { width: 70px; }
+    .commitHeader td { white-space: pre; }
+
+    .commitMessageBox { margin: 2px; }
+    .commitMessage {
+      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+      border: 1px solid white;
+      background-color: white;
+      font-family: monospace;
+      white-space: pre;
+      width: 47em;
+    }
+  </ui:style>
+  <g:HTMLPanel>
+    <table class='{style.commitHeader}'>
+      <tr>
+        <th><ui:msg>Commit</ui:msg></th>
+        <td ui:field='commitName'/>
+        <td><a ui:field='browserLink' href=""/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Author</ui:msg></th>
+        <td><x:InlineHyperlink ui:field='authorNameEmail'
+              title='Search for changes by this user'>
+              <ui:attribute name='title'/>
+            </x:InlineHyperlink>
+        </td>
+        <td ui:field='authorDate'/>
+      </tr>
+      <tr>
+        <th><ui:msg>Committer</ui:msg></th>
+        <td><x:InlineHyperlink ui:field='committerNameEmail'
+              title='Search for changes by this user'>
+              <ui:attribute name='title'/>
+            </x:InlineHyperlink>
+        </td>
+        <td ui:field='committerDate'/>
+      </tr>
+    </table>
+
+    <div class='{style.commitMessageBox}'>
+      <div class='{style.commitMessage}' ui:field='commitMessageText'/>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
new file mode 100644
index 0000000..fcc369d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+interface Constants extends com.google.gwt.i18n.client.Constants {
+  String previousChange();
+  String nextChange();
+  String openChange();
+  String reviewedFileTitle();
+
+  String ps();
+  String commit();
+  String date();
+  String author();
+  String draft();
+  String draftCommentsTooltip();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties
new file mode 100644
index 0000000..b8d14f3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties
@@ -0,0 +1,11 @@
+previousChange = Previous related change
+nextChange = Next related change
+openChange = Open related change
+reviewedFileTitle = Mark file as reviewed (Shortcut: r)
+
+ps = PS
+commit = Commit
+date = Date
+author = Author / Committer
+draft = (DRAFT)
+draftCommentsTooltip = Draft comment(s) inside
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
new file mode 100644
index 0000000..891cc10
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+class DownloadAction extends RightSidePopdownAction {
+  private final DownloadBox downloadBox;
+
+  DownloadAction(
+      ChangeInfo info,
+      String revision,
+      ChangeScreen2.Style style,
+      UIObject relativeTo,
+      Widget downloadButton) {
+    super(style, relativeTo, downloadButton);
+    this.downloadBox = new DownloadBox(info, revision,
+        new PatchSet.Id(info.legacy_id(),
+            info.revision(revision)._number()));
+  }
+
+  Widget getWidget() {
+    return downloadBox;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
new file mode 100644
index 0000000..7c57620
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -0,0 +1,262 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.FetchInfo;
+import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+
+import java.util.EnumSet;
+
+class DownloadBox extends VerticalPanel {
+  private final ChangeInfo change;
+  private final String revision;
+  private final PatchSet.Id psId;
+  private final FlexTable commandTable;
+  private final ListBox scheme;
+  private NativeMap<FetchInfo> fetch;
+
+  DownloadBox(ChangeInfo change, String revision, PatchSet.Id psId) {
+    this.change = change;
+    this.revision = revision;
+    this.psId = psId;
+    this.commandTable = new FlexTable();
+    this.scheme = new ListBox();
+    this.scheme.addChangeHandler(new ChangeHandler() {
+      @Override
+      public void onChange(ChangeEvent event) {
+        renderCommands();
+        if (Gerrit.isSignedIn()) {
+          saveScheme();
+        }
+      }
+    });
+
+    setStyleName(Gerrit.RESOURCES.css().downloadBox());
+    commandTable.setStyleName(Gerrit.RESOURCES.css().downloadBoxTable());
+    scheme.setStyleName(Gerrit.RESOURCES.css().downloadBoxScheme());
+    add(commandTable);
+  }
+
+  @Override
+  protected void onLoad() {
+    if (fetch == null) {
+      RestApi call = ChangeApi.detail(change.legacy_id().get());
+      ChangeList.addOptions(call, EnumSet.of(
+          revision.equals(change.current_revision())
+             ? ListChangesOption.CURRENT_REVISION
+             : ListChangesOption.ALL_REVISIONS,
+          ListChangesOption.DOWNLOAD_COMMANDS));
+      call.get(new AsyncCallback<ChangeInfo>() {
+        @Override
+        public void onSuccess(ChangeInfo result) {
+          fetch = result.revision(revision).fetch();
+          renderScheme();
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      });
+    }
+  }
+
+  private void renderCommands() {
+    commandTable.removeAllRows();
+
+    if (scheme.getItemCount() > 0) {
+      FetchInfo fetchInfo =
+          fetch.get(scheme.getValue(scheme.getSelectedIndex()));
+      for (String commandName : Natives.keys(fetchInfo.commands())) {
+        CopyableLabel copyLabel =
+            new CopyableLabel(fetchInfo.command(commandName));
+        copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadBoxCopyLabel());
+        insertCommand(commandName, copyLabel);
+      }
+    }
+    insertPatch();
+    insertCommand(null, scheme);
+  }
+
+  private void insertPatch() {
+    String id = revision.substring(0, 7);
+    Anchor patchBase64 = new Anchor(id + ".diff.base64");
+    patchBase64.setHref(new RestApi("/changes/")
+      .id(psId.getParentKey().get())
+      .view("revisions")
+      .id(revision)
+      .view("patch")
+      .addParameterTrue("download")
+      .url());
+
+    Anchor patchZip = new Anchor(id + ".diff.zip");
+    patchZip.setHref(new RestApi("/changes/")
+      .id(psId.getParentKey().get())
+      .view("revisions")
+      .id(revision)
+      .view("patch")
+      .addParameterTrue("zip")
+      .url());
+
+    HorizontalPanel p = new HorizontalPanel();
+    p.add(patchBase64);
+    InlineLabel spacer = new InlineLabel("|");
+    spacer.setStyleName(Gerrit.RESOURCES.css().downloadBoxSpacer());
+    p.add(spacer);
+    p.add(patchZip);
+    insertCommand("Patch-File", p);
+  }
+
+  private void insertCommand(String commandName, Widget w) {
+    int row = commandTable.getRowCount();
+    commandTable.insertRow(row);
+    commandTable.getCellFormatter().addStyleName(row, 0,
+        Gerrit.RESOURCES.css().downloadBoxTableCommandColumn());
+    if (commandName != null) {
+      commandTable.setText(row, 0, commandName);
+    }
+    if (w != null) {
+      commandTable.setWidget(row, 1, w);
+    }
+  }
+
+  private void renderScheme() {
+    for (String id : fetch.keySet()) {
+      scheme.addItem(id);
+    }
+    if (scheme.getItemCount() == 0) {
+      scheme.setVisible(false);
+    } else {
+      if (scheme.getItemCount() == 1) {
+        scheme.setSelectedIndex(0);
+        scheme.setVisible(false);
+      } else {
+        int select = 0;
+        String find = getUserPreference();
+        if (find != null) {
+          for (int i = 0; i < scheme.getItemCount(); i++) {
+            if (find.equals(scheme.getValue(i))) {
+              select = i;
+              break;
+            }
+          }
+        }
+        scheme.setSelectedIndex(select);
+      }
+    }
+    renderCommands();
+  }
+
+  private static String getUserPreference() {
+    if (Gerrit.isSignedIn()) {
+      DownloadScheme pref =
+          Gerrit.getUserAccount().getGeneralPreferences().getDownloadUrl();
+      if (pref != null) {
+        switch (pref) {
+          case ANON_GIT:
+            return "git";
+          case ANON_HTTP:
+            return "anonymous http";
+          case HTTP:
+            return "http";
+          case SSH:
+            return "ssh";
+          case REPO_DOWNLOAD:
+            return "repo";
+          default:
+            return null;
+        }
+      }
+    }
+    return null;
+  }
+
+  private void saveScheme() {
+    DownloadScheme scheme = getSelectedScheme();
+    AccountGeneralPreferences pref =
+        Gerrit.getUserAccount().getGeneralPreferences();
+
+    if (scheme != null && scheme != pref.getDownloadUrl()) {
+      pref.setDownloadUrl(scheme);
+      PreferenceInput in = PreferenceInput.create();
+      in.download_scheme(scheme);
+      AccountApi.self().view("preferences")
+          .post(in, new AsyncCallback<JavaScriptObject>() {
+            @Override
+            public void onSuccess(JavaScriptObject result) {
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          });
+    }
+  }
+
+  private DownloadScheme getSelectedScheme() {
+    String id = scheme.getValue(scheme.getSelectedIndex());
+    if ("git".equals(id)) {
+      return DownloadScheme.ANON_GIT;
+    } else if ("anonymous http".equals(id)) {
+      return DownloadScheme.ANON_HTTP;
+    } else if ("http".equals(id)) {
+      return DownloadScheme.HTTP;
+    } else if ("ssh".equals(id)) {
+      return DownloadScheme.SSH;
+    } else if ("repo".equals(id)) {
+      return DownloadScheme.REPO_DOWNLOAD;
+    }
+    return null;
+  }
+
+  private static class PreferenceInput extends JavaScriptObject {
+    static PreferenceInput create() {
+      return createObject().cast();
+    }
+
+    final void download_scheme(DownloadScheme s) {
+      download_scheme0(s.name());
+    }
+
+    private final native void download_scheme0(String n) /*-{
+      this.download_scheme = n;
+    }-*/;
+
+    protected PreferenceInput() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
new file mode 100644
index 0000000..808e4f0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.SubmitFailureDialog;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+class DraftActions {
+
+  static void publish(Change.Id id, String revision) {
+    ChangeApi.publish(id.get(), revision, cs(id));
+  }
+
+  static void delete(Change.Id id, String revision) {
+    ChangeApi.deleteRevision(id.get(), revision, cs(id));
+  }
+
+  static void delete(Change.Id id) {
+    ChangeApi.deleteChange(id.get(), mine());
+  }
+
+  private static GerritCallback<JavaScriptObject> cs(
+      final Change.Id id) {
+    return new GerritCallback<JavaScriptObject>() {
+      public void onSuccess(JavaScriptObject result) {
+        Gerrit.display(PageLinks.toChange(id));
+      }
+
+      public void onFailure(Throwable err) {
+        if (SubmitFailureDialog.isConflict(err)) {
+          new SubmitFailureDialog(err.getMessage()).center();
+          Gerrit.display(PageLinks.toChange(id));
+        } else {
+          super.onFailure(err);
+        }
+      }
+    };
+  }
+
+  private static AsyncCallback<JavaScriptObject> mine() {
+    return new GerritCallback<JavaScriptObject>() {
+      public void onSuccess(JavaScriptObject result) {
+        Gerrit.display(PageLinks.MINE);
+      }
+
+      public void onFailure(Throwable err) {
+        if (SubmitFailureDialog.isConflict(err)) {
+          new SubmitFailureDialog(err.getMessage()).center();
+          Gerrit.display(PageLinks.MINE);
+        } else {
+          super.onFailure(err);
+        }
+      }
+    };
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java
new file mode 100644
index 0000000..a0f7c9d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+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.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class EditMessageAction {
+  private final Change.Id changeId;
+  private final String revision;
+  private final String originalMessage;
+  private final ChangeScreen2.Style style;
+  private final Widget editMessageButton;
+  private final Widget replyButton;
+
+  private EditMessageBox editBox;
+  private PopupPanel popup;
+
+  EditMessageAction(
+      Change.Id changeId,
+      String revision,
+      String originalMessage,
+      ChangeScreen2.Style style,
+      Widget editButton,
+      Widget replyButton) {
+    this.changeId = changeId;
+    this.revision = revision;
+    this.originalMessage = originalMessage;
+    this.style = style;
+    this.editMessageButton = editButton;
+    this.replyButton = replyButton;
+  }
+
+  void onEdit() {
+    if (popup != null) {
+      popup.hide();
+      return;
+    }
+
+    if (editBox == null) {
+      editBox = new EditMessageBox(
+          changeId,
+          revision,
+          originalMessage);
+    }
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(editMessageButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(editBox);
+    p.showRelativeTo(replyButton);
+    GlobalKey.dialog(p);
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
new file mode 100644
index 0000000..cd1f304
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.TextBoxChangeListener;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.ClickEvent;
+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.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+class EditMessageBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, EditMessageBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final Change.Id changeId;
+  private final String revision;
+  private String originalMessage;
+
+  @UiField NpTextArea message;
+  @UiField Button save;
+  @UiField Button cancel;
+
+  EditMessageBox(
+      Change.Id changeId,
+      String revision,
+      String msg) {
+    this.changeId = changeId;
+    this.revision = revision;
+    this.originalMessage = msg.trim();
+    initWidget(uiBinder.createAndBindUi(this));
+    new TextBoxChangeListener(message) {
+      public void onTextChanged(String newText) {
+        save.setEnabled(!newText.trim()
+            .equals(originalMessage));
+      }
+    };
+  }
+
+  @Override
+  protected void onLoad() {
+    message.setText(originalMessage);
+    save.setEnabled(false);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        message.setFocus(true);
+      }});
+  }
+
+  @UiHandler("save")
+  void onSave(ClickEvent e) {
+    ChangeApi.message(changeId.get(), revision, message.getText().trim(),
+        new GerritCallback<JavaScriptObject>() {
+          @Override
+          public void onSuccess(JavaScriptObject msg) {
+            Gerrit.display(PageLinks.toChange(changeId));
+            hide();
+          };
+        });
+  }
+
+  @UiHandler("cancel")
+  void onCancel(ClickEvent e) {
+    hide();
+  }
+
+  private void hide() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide();
+        break;
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
new file mode 100644
index 0000000..118409e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style>
+    .commitMessage {
+      background-color: white;
+      font-family: monospace;
+    }
+    .cancel { float: right; }
+  </ui:style>
+  <g:HTMLPanel>
+    <div class='{res.style.section}'>
+      <c:NpTextArea
+         visibleLines='30'
+         characterWidth='72'
+         styleName='{style.commitMessage}'
+         ui:field='message'/>
+    </div>
+    <div class='{res.style.section}'>
+      <g:Button ui:field='save'
+          title='Create new patch set with updated commit message'
+          styleName='{res.style.button}'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Save</ui:msg></div>
+      </g:Button>
+      <g:Button ui:field='cancel'
+          styleName='{res.style.button}'
+          addStyleNames='{style.cancel}'>
+          <div>Cancel</div>
+      </g:Button>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
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
new file mode 100644
index 0000000..77a0091
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -0,0 +1,627 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.ReviewInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.InputElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.progress.client.ProgressBar;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.sql.Timestamp;
+
+class FileTable extends FlowPanel {
+  static final FileTableResources R = GWT
+      .create(FileTableResources.class);
+
+  interface FileTableResources extends ClientBundle {
+    @Source("file_table.css")
+    FileTableCss css();
+  }
+
+  interface FileTableCss extends CssResource {
+    String pointer();
+    String reviewed();
+    String status();
+    String pathColumn();
+    String draftColumn();
+    String newColumn();
+    String commentColumn();
+    String deltaColumn1();
+    String deltaColumn2();
+    String commonPrefix();
+    String inserted();
+    String deleted();
+  }
+
+  private static final String REVIEWED;
+  private static final String OPEN;
+  private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+
+  static {
+    REVIEWED = DOM.createUniqueId().replace('-', '_');
+    OPEN = DOM.createUniqueId().replace('-', '_');
+    init(REVIEWED, OPEN);
+  }
+
+  private static final native void init(String r, String o) /*-{
+    $wnd[r] = $entry(function(e,i) {
+      @com.google.gerrit.client.change.FileTable::onReviewed(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
+    });
+    $wnd[o] = $entry(function(e,i) {
+      return @com.google.gerrit.client.change.FileTable::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
+    });
+  }-*/;
+
+  private static void onReviewed(NativeEvent e, int idx) {
+    MyTable t = getMyTable(e);
+    if (t != null) {
+      t.onReviewed(InputElement.as(Element.as(e.getEventTarget())), idx);
+    }
+  }
+
+  private static boolean onOpen(NativeEvent e, int idx) {
+    if (link.handleAsClick(e.<Event> cast())) {
+      MyTable t = getMyTable(e);
+      if (t != null) {
+        t.onOpenRow(1 + idx);
+        e.preventDefault();
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static MyTable getMyTable(NativeEvent event) {
+    com.google.gwt.user.client.Element e = event.getEventTarget().cast();
+    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+      EventListener l = DOM.getEventListener(e);
+      if (l instanceof MyTable) {
+        return (MyTable) l;
+      }
+    }
+    return null;
+  }
+
+  private PatchSet.Id base;
+  private PatchSet.Id curr;
+  private MyTable table;
+  private boolean register;
+  private JsArrayString reviewed;
+  private String scrollToPath;
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    R.css().ensureInjected();
+  }
+
+  void setRevisions(PatchSet.Id base, PatchSet.Id curr) {
+    this.base = base;
+    this.curr = curr;
+  }
+
+  void setValue(NativeMap<FileInfo> fileMap,
+      Timestamp myLastReply,
+      NativeMap<JsArray<CommentInfo>> comments,
+      NativeMap<JsArray<CommentInfo>> drafts) {
+    JsArray<FileInfo> list = fileMap.values();
+    FileInfo.sortFileInfoByPath(list);
+
+    DisplayCommand cmd = new DisplayCommand(fileMap, list,
+        myLastReply, comments, drafts);
+    if (cmd.execute()) {
+      cmd.showProgressBar();
+      Scheduler.get().scheduleIncremental(cmd);
+    }
+  }
+
+  void markReviewed(JsArrayString reviewed) {
+    if (table != null) {
+      table.markReviewed(reviewed);
+    } else {
+      this.reviewed = reviewed;
+    }
+  }
+
+  void registerKeys() {
+    register = true;
+
+    if (table != null) {
+      table.setRegisterKeys(true);
+    }
+  }
+
+  void scrollToPath(String path) {
+    if (table != null) {
+      table.scrollToPath(path);
+    } else {
+      scrollToPath = path;
+    }
+  }
+
+  private void setTable(MyTable table) {
+    clear();
+    add(table);
+    this.table = table;
+
+    if (register) {
+      table.setRegisterKeys(true);
+    }
+    if (reviewed != null) {
+      table.markReviewed(reviewed);
+      reviewed = null;
+    }
+    if (scrollToPath != null) {
+      table.scrollToPath(scrollToPath);
+      scrollToPath = null;
+    }
+  }
+
+  private String url(FileInfo info) {
+    // TODO(sop): Switch to Dispatcher.toPatchSideBySide.
+    Change.Id c = curr.getParentKey();
+    StringBuilder p = new StringBuilder();
+    p.append("/c/").append(c).append('/');
+    if (base != null) {
+      p.append(base.get()).append("..");
+    }
+    p.append(curr.get()).append('/').append(KeyUtil.encode(info.path()));
+    p.append(info.binary() ? ",unified" : ",cm");
+    return p.toString();
+  }
+
+  private final class MyTable extends NavigationTable<FileInfo> {
+    private final NativeMap<FileInfo> map;
+    private final JsArray<FileInfo> list;
+
+    MyTable(NativeMap<FileInfo> map, JsArray<FileInfo> list) {
+      this.map = map;
+      this.list = list;
+      table.setWidth("");
+
+      keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()));
+      keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
+      keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
+      keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
+          Util.C.patchTableOpenDiff()));
+
+      keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          int row = getCurrentRow();
+          if (1 <= row && row <= MyTable.this.list.length()) {
+            FileInfo info = MyTable.this.list.get(row - 1);
+            InputElement b = getReviewed(info);
+            boolean c = !b.isChecked();
+            setReviewed(info, c);
+            b.setChecked(c);
+          }
+        }
+      });
+
+      setSavePointerId(
+          (base != null ? base.toString() + ".." : "")
+          + curr.toString());
+    }
+
+    void onReviewed(InputElement checkbox, int idx) {
+      setReviewed(list.get(idx), checkbox.isChecked());
+    }
+
+    private void setReviewed(FileInfo info, boolean r) {
+      RestApi api = ChangeApi.revision(curr)
+          .view("files")
+          .id(info.path())
+          .view("reviewed");
+      if (r) {
+        api.put(CallbackGroup.<ReviewInfo>emptyCallback());
+      } else {
+        api.delete(CallbackGroup.<ReviewInfo>emptyCallback());
+      }
+    }
+
+    void markReviewed(JsArrayString reviewed) {
+      for (int i = 0; i < reviewed.length(); i++) {
+        FileInfo info = map.get(reviewed.get(i));
+        if (info != null) {
+          getReviewed(info).setChecked(true);
+        }
+      }
+    }
+
+    private InputElement getReviewed(FileInfo info) {
+      CellFormatter fmt = table.getCellFormatter();
+      Element e = fmt.getElement(1 + info._row(), 1);
+      return InputElement.as(e.getFirstChildElement());
+    }
+
+    void scrollToPath(String path) {
+      FileInfo info = map.get(path);
+      if (info != null) {
+        movePointerTo(1 + info._row(), true);
+      }
+    }
+
+    @Override
+    protected Object getRowItemKey(FileInfo item) {
+      return item.path();
+    }
+
+    @Override
+    protected int findRow(Object id) {
+      FileInfo info = map.get((String) id);
+      return info != null ? 1 + info._row() : -1;
+    }
+
+    @Override
+    protected FileInfo getRowItem(int row) {
+      if (1 <= row && row <= list.length()) {
+        return list.get(row - 1);
+      }
+      return null;
+    }
+
+    @Override
+    protected void onOpenRow(int row) {
+      if (1 <= row && row <= list.length()) {
+        Gerrit.display(url(list.get(row - 1)));
+      }
+    }
+  }
+
+  private final class DisplayCommand implements RepeatingCommand {
+    private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
+    private final MyTable table;
+    private final JsArray<FileInfo> list;
+    private final Timestamp myLastReply;
+    private final NativeMap<JsArray<CommentInfo>> comments;
+    private final NativeMap<JsArray<CommentInfo>> drafts;
+    private final boolean hasUser;
+    private boolean attached;
+    private int row;
+    private double start;
+    private ProgressBar meter;
+    private String lastPath = "";
+
+    private int inserted;
+    private int deleted;
+
+    private DisplayCommand(NativeMap<FileInfo> map,
+        JsArray<FileInfo> list,
+        Timestamp myLastReply,
+        NativeMap<JsArray<CommentInfo>> comments,
+        NativeMap<JsArray<CommentInfo>> drafts) {
+      this.table = new MyTable(map, list);
+      this.list = list;
+      this.myLastReply = myLastReply;
+      this.comments = comments;
+      this.drafts = drafts;
+      this.hasUser = Gerrit.isSignedIn();
+    }
+
+    public boolean execute() {
+      boolean attachedNow = isAttached();
+      if (!attached && attachedNow) {
+        // Remember that we have been attached at least once. If
+        // later we find we aren't attached we should stop running.
+        attached = true;
+      } else if (attached && !attachedNow) {
+        // If the user navigated away, we aren't in the DOM anymore.
+        // Don't continue to render.
+        return false;
+      }
+
+      start = System.currentTimeMillis();
+      if (row == 0) {
+        header(sb);
+        computeInsertedDeleted();
+      }
+      while (row < list.length()) {
+        FileInfo info = list.get(row);
+        info._row(row);
+        render(sb, info);
+        if ((++row % 10) == 0 && longRunning()) {
+          updateMeter();
+          return true;
+        }
+      }
+      footer(sb);
+      table.resetHtml(sb);
+      table.finishDisplay();
+      setTable(table);
+      return false;
+    }
+
+    private void computeInsertedDeleted() {
+      inserted = 0;
+      deleted = 0;
+      for (int i = 0; i < list.length(); i++) {
+        FileInfo info = list.get(i);
+        if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
+          inserted += info.lines_inserted();
+          deleted += info.lines_deleted();
+        }
+      }
+    }
+
+    void showProgressBar() {
+      if (meter == null) {
+        meter = new ProgressBar(Util.M.loadingPatchSet(curr.get()));
+        FileTable.this.clear();
+        FileTable.this.add(meter);
+      }
+      updateMeter();
+    }
+
+    void updateMeter() {
+      if (meter != null) {
+        int n = list.length();
+        meter.setValue((100 * row) / n);
+      }
+    }
+
+    private boolean longRunning() {
+      return System.currentTimeMillis() - start > 200;
+    }
+
+    private void header(SafeHtmlBuilder sb) {
+      sb.openTr();
+      sb.openTh().setStyleName(R.css().pointer()).closeTh();
+      sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+      sb.openTh().setStyleName(R.css().status()).closeTh();
+      sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
+      sb.openTh()
+        .setAttribute("colspan", 3)
+        .append(Util.C.patchTableColumnComments())
+        .closeTh();
+      sb.openTh()
+        .setAttribute("colspan", 2)
+        .append(Util.C.patchTableColumnSize())
+        .closeTh();
+      sb.closeTr();
+    }
+
+    private void render(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTr();
+      sb.openTd().setStyleName(R.css().pointer()).closeTd();
+      columnReviewed(sb, info);
+      columnStatus(sb, info);
+      columnPath(sb, info);
+      columnComments(sb, info);
+      columnDelta1(sb, info);
+      columnDelta2(sb, info);
+      sb.closeTr();
+    }
+
+    private void columnReviewed(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd().setStyleName(R.css().reviewed());
+      if (hasUser) {
+        sb.openElement("input")
+          .setAttribute("title", Resources.C.reviewedFileTitle())
+          .setAttribute("type", "checkbox")
+          .setAttribute("onclick", REVIEWED + "(event," + info._row() + ")")
+          .closeSelf();
+      }
+      sb.closeTd();
+    }
+
+    private void columnStatus(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd().setStyleName(R.css().status());
+      if (!Patch.COMMIT_MSG.equals(info.path())
+          && info.status() != null
+          && !ChangeType.MODIFIED.matches(info.status())) {
+        sb.append(info.status());
+      }
+      sb.closeTd();
+    }
+
+    private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd()
+        .setStyleName(R.css().pathColumn())
+        .openAnchor()
+        .setAttribute("href", "#" + url(info))
+        .setAttribute("onclick", OPEN + "(event," + info._row() + ")");
+
+      String path = info.path();
+      if (Patch.COMMIT_MSG.equals(path)) {
+        sb.append(Util.C.commitMessage());
+      } else {
+        int commonPrefixLen = commonPrefix(path);
+        if (commonPrefixLen > 0) {
+          sb.openSpan().setStyleName(R.css().commonPrefix())
+            .append(path.substring(0, commonPrefixLen))
+            .closeSpan();
+        }
+        sb.append(path.substring(commonPrefixLen));
+        lastPath = path;
+      }
+
+      sb.closeAnchor()
+        .closeTd();
+    }
+
+    private int commonPrefix(String path) {
+      for (int n = path.length(); n > 0;) {
+        int s = path.lastIndexOf('/', n);
+        if (s < 0) {
+          return 0;
+        }
+
+        String p = path.substring(0, s + 1);
+        if (lastPath.startsWith(p)) {
+          return s + 1;
+        }
+        n = s - 1;
+      }
+      return 0;
+    }
+
+    private void columnComments(SafeHtmlBuilder sb, FileInfo info) {
+      JsArray<CommentInfo> cList = get(info.path(), comments);
+      JsArray<CommentInfo> dList = get(info.path(), drafts);
+
+      sb.openTd().setStyleName(R.css().draftColumn());
+      if (dList.length() > 0) {
+        sb.append("drafts: ").append(dList.length());
+      }
+      sb.closeTd();
+
+      int cntAll = cList.length();
+      int cntNew = 0;
+      if (myLastReply != null) {
+        for (int i = cntAll - 1; i >= 0; i--) {
+          CommentInfo m = cList.get(i);
+          if (m.updated().compareTo(myLastReply) > 0) {
+            cntNew++;
+          } else {
+            break;
+          }
+        }
+      }
+
+      sb.openTd().setStyleName(R.css().newColumn());
+      if (cntNew > 0) {
+        sb.append("new: ").append(cntNew);
+      }
+      sb.closeTd();
+
+      sb.openTd().setStyleName(R.css().commentColumn());
+      if (cntAll - cntNew > 0) {
+        sb.append("comments: ").append(cntAll - cntNew);
+      }
+      sb.closeTd();
+    }
+
+    private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
+      JsArray<CommentInfo> r =  m.get(p);
+      if (r == null) {
+        r = JsArray.createArray().cast();
+      }
+      return r;
+    }
+
+    private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd().setStyleName(R.css().deltaColumn1());
+      if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
+        sb.append(info.lines_inserted() + info.lines_deleted());
+      }
+      sb.closeTd();
+    }
+
+    private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd().setStyleName(R.css().deltaColumn2());
+      if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
+          && (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
+        int w = 80;
+        int t = inserted + deleted;
+        int i = Math.max(5, (int) (((double) w) * info.lines_inserted() / t));
+        int d = Math.max(5, (int) (((double) w) * info.lines_deleted() / t));
+
+        sb.setAttribute(
+            "title",
+            Util.M.patchTableSize_LongModify(info.lines_inserted(),
+                info.lines_deleted()));
+
+        if (0 < info.lines_inserted()) {
+          sb.openDiv()
+            .setStyleName(R.css().inserted())
+            .setAttribute("style", "width:" + i + "px")
+            .closeDiv();
+        }
+        if (0 < info.lines_deleted()) {
+          sb.openDiv()
+            .setStyleName(R.css().deleted())
+            .setAttribute("style", "width:" + d + "px")
+            .closeDiv();
+        }
+      }
+      sb.closeTd();
+    }
+
+    private void footer(SafeHtmlBuilder sb) {
+      sb.openTr();
+      sb.openTh().setStyleName(R.css().pointer()).closeTh();
+      sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+      sb.openTh().setStyleName(R.css().status()).closeTh();
+      sb.openTd().closeTd(); // path
+      sb.openTd().setAttribute("colspan", 3).closeTd(); // comments
+
+      // delta1
+      sb.openTh().setStyleName(R.css().deltaColumn1())
+        .append(Util.M.patchTableSize_Modify(inserted, deleted))
+        .closeTh();
+
+      // delta2
+      sb.openTh().setStyleName(R.css().deltaColumn2());
+      int w = 80;
+      int t = inserted + deleted;
+      int i = Math.max(1, (int) (((double) w) * inserted / t));
+      int d = Math.max(1, (int) (((double) w) * deleted / t));
+      if (i + d > w && i > d) {
+        i = w - d;
+      } else if (i + d > w && d > i) {
+        d = w - i;
+      }
+      if (0 < inserted) {
+        sb.openDiv()
+        .setStyleName(R.css().inserted())
+        .setAttribute("style", "width:" + i + "px")
+        .closeDiv();
+      }
+      if (0 < deleted) {
+        sb.openDiv()
+          .setStyleName(R.css().deleted())
+          .setAttribute("style", "width:" + d + "px")
+          .closeDiv();
+      }
+      sb.closeTh();
+
+      sb.closeTr();
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
new file mode 100644
index 0000000..58b286c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+class IncludedInAction extends RightSidePopdownAction {
+  private final IncludedInBox includedInBox;
+
+  IncludedInAction(
+      Change.Id changeId,
+      ChangeScreen2.Style style,
+      UIObject relativeTo,
+      Widget includedInButton) {
+    super(style, relativeTo, includedInButton);
+    this.includedInBox = new IncludedInBox(changeId);
+  }
+
+  Widget getWidget() {
+    return includedInBox;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
new file mode 100644
index 0000000..927e500
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo.IncludedInInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class IncludedInBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, IncludedInBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface Style extends CssResource {
+    String includedInElement();
+  }
+
+  private final Change.Id changeId;
+  private boolean loaded;
+
+  @UiField Style style;
+  @UiField Element branches;
+  @UiField Element tags;
+
+  IncludedInBox(Change.Id changeId) {
+    this.changeId = changeId;
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  @Override
+  protected void onLoad() {
+    if (!loaded) {
+      ChangeApi.includedIn(changeId.get(),
+          new AsyncCallback<IncludedInInfo>() {
+        @Override
+        public void onSuccess(IncludedInInfo r) {
+          branches.setInnerSafeHtml(formatList(r.branches()));
+          tags.setInnerSafeHtml(formatList(r.tags()));
+          loaded = true;
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      });
+    }
+  }
+
+  private SafeHtml formatList(JsArrayString l) {
+    SafeHtmlBuilder html = new SafeHtmlBuilder();
+    int size = l.length();
+    for (int i = 0; i < size; i++) {
+      html.openSpan()
+          .addStyleName(style.includedInElement())
+          .append(l.get(i))
+          .closeSpan();
+      if (i < size - 1) {
+        html.append(", ");
+      }
+    }
+    return html;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
new file mode 100644
index 0000000..59b05f0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:style type='com.google.gerrit.client.change.IncludedInBox.Style'>
+    .includedInBox {
+      min-width: 300px;
+      max-width: 580px;
+      margin: 5px;
+    }
+
+    .includedInTable {
+      border-spacing: 0;
+    }
+
+    .includedInTable th {
+      width: 60px;
+      color: #444;
+      font-weight: normal;
+      vertical-align: top;
+      text-align: left;
+      padding-right: 5px;
+    }
+
+    .includedInElement {
+      font-size: smaller;
+      font-family: monospace;
+    }
+
+    .includedInElement span {
+      width: 500px;
+      white-space: nowrap;
+      display: inline-block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+
+    .includedInElement .gwt-TextBox {
+      padding: 0;
+      margin: 0;
+      border: 0;
+      max-height: 18px;
+      width: 500px;
+    }
+
+    .includedInElement div {
+      float: right;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.includedInBox}'>
+    <table class='{style.includedInTable}'>
+      <tr>
+        <th><ui:msg>Branches</ui:msg></th>
+          <td ui:field='branches'/>
+      </tr>
+      <tr>
+        <th><ui:msg>Tags</ui:msg></th>
+          <td ui:field='tags'/>
+      </tr>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
new file mode 100644
index 0000000..099091c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -0,0 +1,295 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.account.AccountInfo.AvatarInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Displays a table of label and reviewer scores. */
+class Labels extends Grid {
+  private static final String DATA_ID = "data-id";
+  private static final String REMOVE;
+
+  static {
+    REMOVE = DOM.createUniqueId().replace('-', '_');
+    init(REMOVE);
+  }
+
+  private static final native void init(String r) /*-{
+    $wnd[r] = $entry(function(e) {
+      @com.google.gerrit.client.change.Labels::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+    });
+  }-*/;
+
+  private static void onRemove(NativeEvent event) {
+    Integer user = getDataId(event);
+    if (user != null) {
+      final ChangeScreen2 screen = ChangeScreen2.get(event);
+      ChangeApi.reviewer(screen.getChangeId().get(), user).delete(
+          new GerritCallback<JavaScriptObject>() {
+            @Override
+            public void onSuccess(JavaScriptObject result) {
+              if (screen.isCurrentView()) {
+                Gerrit.display(PageLinks.toChange(screen.getChangeId()));
+              }
+            }
+          });
+    }
+  }
+
+  private static Integer getDataId(NativeEvent event) {
+    Element e = event.getEventTarget().cast();
+    while (e != null) {
+      String v = e.getAttribute(DATA_ID);
+      if (!v.isEmpty()) {
+        return Integer.parseInt(v);
+      }
+      e = e.getParentElement();
+    }
+    return null;
+  }
+
+  private ChangeScreen2.Style style;
+  private Element statusText;
+
+  void init(ChangeScreen2.Style style, Element statusText) {
+    this.style = style;
+    this.statusText = statusText;
+  }
+
+  boolean set(ChangeInfo info, boolean current) {
+    List<String> names = new ArrayList<String>(info.labels());
+    Collections.sort(names);
+
+    boolean canSubmit = info.status().isOpen();
+    resize(names.size(), 2);
+
+    for (int row = 0; row < names.size(); row++) {
+      String name = names.get(row);
+      LabelInfo label = info.label(name);
+      setText(row, 0, name);
+      if (label.all() != null) {
+        setWidget(row, 1, renderUsers(label));
+      }
+      getCellFormatter().setStyleName(row, 0, style.labelName());
+      getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
+
+      if (canSubmit && info.status() == Change.Status.NEW) {
+        switch (label.status()) {
+          case NEED:
+            if (current) {
+              statusText.setInnerText("Needs " + name);
+            }
+            canSubmit = false;
+            break;
+          case REJECT:
+          case IMPOSSIBLE:
+            if (current) {
+              statusText.setInnerText("Not " + name);
+            }
+            canSubmit = false;
+            break;
+          default:
+            break;
+          }
+      }
+    }
+    return canSubmit;
+  }
+
+  private Widget renderUsers(LabelInfo label) {
+    Map<Integer, List<ApprovalInfo>> m = new HashMap<Integer, List<ApprovalInfo>>(4);
+    int approved = 0, rejected = 0;
+
+    for (ApprovalInfo ai : Natives.asList(label.all())) {
+      if (ai.value() != 0) {
+        List<ApprovalInfo> l = m.get(Integer.valueOf(ai.value()));
+        if (l == null) {
+          l = new ArrayList<ApprovalInfo>(label.all().length());
+          m.put(Integer.valueOf(ai.value()), l);
+        }
+        l.add(ai);
+
+        if (isRejected(label, ai)) {
+          rejected = ai.value();
+        } else if (isApproved(label, ai)) {
+          approved = ai.value();
+        }
+      }
+    }
+
+    SafeHtmlBuilder html = new SafeHtmlBuilder();
+    for (Integer v : sort(m.keySet(), approved, rejected)) {
+      if (!html.isEmpty()) {
+        html.br();
+      }
+
+      String val = LabelValue.formatValue(v.shortValue());
+      html.openSpan();
+      html.setAttribute("title", label.value_text(val));
+      if (v.intValue() == approved) {
+        html.setStyleName(style.label_ok());
+      } else if (v.intValue() == rejected) {
+        html.setStyleName(style.label_reject());
+      }
+      html.append(val).append(" ");
+      html.append(formatUserList(style, m.get(v),
+          Collections.<Integer> emptySet()));
+      html.closeSpan();
+    }
+    return html.toBlockWidget();
+  }
+
+  private static List<Integer> sort(Set<Integer> keySet, int a, int b) {
+    List<Integer> r = new ArrayList<Integer>(keySet);
+    Collections.sort(r);
+    if (keySet.contains(a)) {
+      r.remove(Integer.valueOf(a));
+      r.add(0, a);
+    } else if (keySet.contains(b)) {
+      r.remove(Integer.valueOf(b));
+      r.add(0, b);
+    }
+    return r;
+  }
+
+  private static boolean isApproved(LabelInfo label, ApprovalInfo ai) {
+    return label.approved() != null
+        && label.approved()._account_id() == ai._account_id();
+  }
+
+  private static boolean isRejected(LabelInfo label, ApprovalInfo ai) {
+    return label.rejected() != null
+        && label.rejected()._account_id() == ai._account_id();
+  }
+
+  private String getStyleForLabel(LabelInfo label) {
+    switch (label.status()) {
+      case OK:
+        return style.label_ok();
+      case NEED:
+        return style.label_need();
+      case REJECT:
+      case IMPOSSIBLE:
+        return style.label_reject();
+      default:
+      case MAY:
+        return style.label_may();
+    }
+  }
+
+  static SafeHtml formatUserList(ChangeScreen2.Style style,
+      Collection<? extends AccountInfo> in,
+      Set<Integer> removable) {
+    List<AccountInfo> users = new ArrayList<AccountInfo>(in);
+    Collections.sort(users, new Comparator<AccountInfo>() {
+      @Override
+      public int compare(AccountInfo a, AccountInfo b) {
+        String as = name(a);
+        String bs = name(b);
+        if (as.isEmpty()) {
+          return 1;
+        } else if (bs.isEmpty()) {
+          return -1;
+        }
+        return as.compareTo(bs);
+      }
+
+      private String name(AccountInfo a) {
+        if (a.name() != null) {
+          return a.name();
+        } else if (a.email() != null) {
+          return a.email();
+        }
+        return "";
+      }
+    });
+
+    SafeHtmlBuilder html = new SafeHtmlBuilder();
+    Iterator<? extends AccountInfo> itr = users.iterator();
+    while (itr.hasNext()) {
+      AccountInfo ai = itr.next();
+      AvatarInfo img = ai.avatar(AvatarInfo.DEFAULT_SIZE);
+      String name;
+      if (ai.name() != null) {
+        name = ai.name();
+      } else if (ai.email() != null) {
+        name = ai.email();
+      } else {
+        name = Integer.toString(ai._account_id());
+      }
+
+      html.openSpan()
+          .setAttribute("role", "listitem")
+          .setAttribute(DATA_ID, ai._account_id())
+          .setStyleName(style.label_user());
+      if (img != null) {
+        html.openElement("img")
+            .setStyleName(style.avatar())
+            .setAttribute("src", img.url());
+        if (img.width() > 0) {
+          html.setAttribute("width", img.width());
+        }
+        if (img.height() > 0) {
+          html.setAttribute("height", img.height());
+        }
+        html.closeSelf();
+      }
+      html.append(name);
+      if (removable.contains(ai._account_id())) {
+        html.openElement("button")
+            .setAttribute("title", Util.M.removeReviewer(name))
+            .setAttribute("onclick", REMOVE + "(event)")
+            .append(new ImageResourceRenderer().render(Resources.I.remove_reviewer()))
+            .closeElement("button");
+      }
+      html.closeSpan();
+      if (itr.hasNext()) {
+        html.append(' ');
+      }
+    }
+    return html;
+  }
+}
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
new file mode 100644
index 0000000..6fcabee
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class Message extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, Message> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  static interface Style extends CssResource {
+    String closed();
+  }
+
+  @UiField Style style;
+  @UiField HTMLPanel header;
+  @UiField Element name;
+  @UiField Element summary;
+  @UiField Element date;
+  @UiField Element message;
+
+  @UiField(provided = true)
+  AvatarImage avatar;
+
+  Message(CommentLinkProcessor clp, MessageInfo info) {
+    if (info.author() != null) {
+      avatar = new AvatarImage(info.author());
+      avatar.setSize("", "");
+    } else {
+      avatar = new AvatarImage();
+    }
+
+    initWidget(uiBinder.createAndBindUi(this));
+    header.addDomHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        setOpen(!isOpen());
+      }
+    }, ClickEvent.getType());
+
+    name.setInnerText(authorName(info));
+    date.setInnerText(FormatUtil.shortFormatDayTime(info.date()));
+    if (info.message() != null) {
+      String msg = info.message().trim();
+      summary.setInnerText(msg);
+      message.setInnerSafeHtml(clp.apply(
+          new SafeHtmlBuilder().append(msg).wikify()));
+    }
+  }
+
+  private boolean isOpen() {
+    return UIObject.isVisible(message);
+  }
+
+  void setOpen(boolean open) {
+    UIObject.setVisible(summary, !open);
+    UIObject.setVisible(message, open);
+    if (open) {
+      removeStyleName(style.closed());
+    } else {
+      addStyleName(style.closed());
+    }
+  }
+
+  static String authorName(MessageInfo info) {
+    if (info.author() != null) {
+      if (info.author().name() != null) {
+        return info.author().name();
+      }
+      return Gerrit.getConfig().getAnonymousCowardName();
+    }
+    return Util.C.messageNoAuthor();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
new file mode 100644
index 0000000..b36fbb2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gerrit.client'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:style type='com.google.gerrit.client.change.Message.Style'>
+    .messageBox {
+      position: relative;
+      width: 1168px;
+      border-left: 1px solid #e3e9ff;
+      border-right: 1px solid #e3e9ff;
+      border-bottom: 1px solid #e3e9ff;
+      -webkit-border-bottom-left-radius: 8px;
+      -webkit-border-bottom-right-radius: 8px;
+    }
+
+    .avatar {
+      position: absolute;
+      width: 26px;
+      height: 26px;
+    }
+    .closed .avatar {
+      position: absolute;
+      width: 16px;
+      height: 16px;
+    }
+
+    .contents {
+      margin-left: 28px;
+      position: relative;
+    }
+
+    .contents p {
+      -webkit-margin-before: 0;
+      -webkit-margin-after: 0.3em;
+    }
+
+    .name {
+      white-space: nowrap;
+      font-weight: bold;
+    }
+    .closed .name {
+      width: 120px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      font-weight: normal;
+    }
+
+    .summary {
+      color: #777;
+      position: absolute;
+      top: 0;
+      left: 120px;
+      width: 915px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .date {
+      white-space: nowrap;
+      position: absolute;
+      top: 0;
+      right: 5px;
+    }
+  </ui:style>
+
+  <g:HTMLPanel
+      styleName='{style.messageBox}'
+      addStyleNames='{style.closed}'>
+    <c:AvatarImage ui:field='avatar' styleName='{style.avatar}'/>
+    <div class='{style.contents}'>
+      <g:HTMLPanel ui:field='header'>
+        <div class='{style.name}' ui:field='name'/>
+        <div ui:field='summary' class='{style.summary}'/>
+        <div class='{style.date}' ui:field='date'/>
+      </g:HTMLPanel>
+      <div ui:field='message'
+           aria-hidden='true'
+           style='display: NONE'/>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
new file mode 100644
index 0000000..ddb7d56
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+/** Applies a label with one mouse click. */
+class QuickApprove extends Button implements ClickHandler {
+  private Change.Id changeId;
+  private String revision;
+  private ReviewInput input;
+
+  QuickApprove() {
+    addClickHandler(this);
+  }
+
+  void set(ChangeInfo info, String commit) {
+    if (!info.has_permitted_labels() || !info.status().isOpen()) {
+      // Quick approve needs at least one label on an open change.
+      setVisible(false);
+      return;
+    }
+
+    String qName = null;
+    String qValueStr = null;
+    short qValue = 0;
+
+    for (LabelInfo label : Natives.asList(info.all_labels().values())) {
+      if (!info.permitted_labels().containsKey(label.name())) {
+        continue;
+      }
+
+      JsArrayString values = info.permitted_values(label.name());
+      if (values.length() == 0) {
+        continue;
+      }
+
+      switch (label.status()) {
+        case NEED: // Label is required for submit.
+          break;
+
+        case OK: // Label already applied.
+        case MAY: // Label is not required.
+          continue;
+
+        case REJECT: // Submit cannot happen, do not quick approve.
+        case IMPOSSIBLE:
+          setVisible(false);
+          return;
+      }
+
+      String s = values.get(values.length() - 1);
+      short v = LabelInfo.parseValue(s);
+      if (v > 0 && s.equals(label.max_value())) {
+        if (qName != null) {
+          // Quick approve is available for one label only.
+          setVisible(false);
+          return;
+        }
+
+        qName = label.name();
+        qValueStr = s;
+        qValue = v;
+      }
+    }
+
+    if (qName != null)  {
+      changeId = info.legacy_id();
+      revision = commit;
+      input = ReviewInput.create();
+      input.label(qName, qValue);
+      setText(qName + qValueStr);
+    } else {
+      setVisible(false);
+    }
+  }
+
+  @Override
+  public void setText(String text) {
+    setHTML(new SafeHtmlBuilder().openDiv().append(text).closeDiv());
+  }
+
+  @Override
+  public void onClick(ClickEvent event) {
+    ChangeApi.revision(changeId.get(), revision)
+      .view("review")
+      .post(input, new GerritCallback<ReviewInput>() {
+        @Override
+        public void onSuccess(ReviewInput result) {
+          Gerrit.display(PageLinks.toChange(changeId));
+        }
+      });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
new file mode 100644
index 0000000..2b1c250
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+
+class RebaseAction {
+  static void call(final Change.Id id, String revision) {
+    ChangeApi.rebase(id.get(), revision,
+      new GerritCallback<ChangeInfo>() {
+        public void onSuccess(ChangeInfo result) {
+          Gerrit.display(PageLinks.toChange(id));
+        }
+      });
+  }
+}
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
new file mode 100644
index 0000000..f8a7cc7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -0,0 +1,345 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+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.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+import com.google.gwtexpui.progress.client.ProgressBar;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class RelatedChanges extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, RelatedChanges> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private static final String OPEN;
+  private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+
+  static {
+    OPEN = DOM.createUniqueId().replace('-', '_');
+    init(OPEN);
+  }
+
+  private static final native void init(String o) /*-{
+    $wnd[o] = $entry(function(e,i) {
+      return @com.google.gerrit.client.change.RelatedChanges::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
+    });
+  }-*/;
+
+  private static boolean onOpen(NativeEvent e, int idx) {
+    if (link.handleAsClick(e.<Event> cast())) {
+      MyTable t = getMyTable(e);
+      if (t != null) {
+        t.onOpenRow(idx);
+        e.preventDefault();
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static MyTable getMyTable(NativeEvent event) {
+    com.google.gwt.user.client.Element e = event.getEventTarget().cast();
+    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+      EventListener l = DOM.getEventListener(e);
+      if (l instanceof MyTable) {
+        return (MyTable) l;
+      }
+    }
+    return null;
+  }
+
+  interface Style extends CssResource {
+    String subject();
+  }
+
+  private String project;
+  private MyTable table;
+  private boolean register;
+
+  @UiField Style style;
+  @UiField Element header;
+  @UiField Element none;
+  @UiField ScrollPanel scroll;
+  @UiField ProgressBar progress;
+  @UiField Element error;
+
+  RelatedChanges() {
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void set(ChangeInfo info, final String revision) {
+    if (info.status().isClosed()) {
+      setVisible(false);
+      return;
+    }
+
+    project = info.project();
+
+    ChangeApi.revision(info.legacy_id().get(), revision)
+      .view("related")
+      .get(new AsyncCallback<RelatedInfo>() {
+        @Override
+        public void onSuccess(RelatedInfo result) {
+          render(revision, result.changes());
+        }
+
+        @Override
+        public void onFailure(Throwable err) {
+          progress.setVisible(false);
+          scroll.setVisible(false);
+          UIObject.setVisible(error, true);
+          error.setInnerText(err.getMessage());
+        }
+      });
+  }
+
+  void setMaxHeight(int height) {
+    int h = height - header.getOffsetHeight();
+    scroll.setHeight(h + "px");
+  }
+
+  void registerKeys() {
+    register = true;
+
+    if (table != null) {
+      table.setRegisterKeys(true);
+    }
+  }
+
+  private void render(String revision, JsArray<ChangeAndCommit> list) {
+    if (0 < list.length()) {
+      DisplayCommand cmd = new DisplayCommand(revision, list);
+      if (cmd.execute()) {
+        Scheduler.get().scheduleIncremental(cmd);
+      }
+    } else {
+      progress.setVisible(false);
+      UIObject.setVisible(header, false);
+      UIObject.setVisible(none, true);
+    }
+  }
+
+  private void setTable(MyTable t) {
+    progress.setVisible(false);
+
+    scroll.clear();
+    scroll.add(t);
+    scroll.setVisible(true);
+    table = t;
+
+    if (register) {
+      table.setRegisterKeys(true);
+    }
+  }
+
+  private String url(ChangeAndCommit c) {
+    if (c.has_change_number() && c.has_revision_number()) {
+      PatchSet.Id id = c.patch_set_id();
+      return "#" + PageLinks.toChange(
+          id.getParentKey(),
+          String.valueOf(id.get()));
+    }
+
+    GitwebLink gw = Gerrit.getGitwebLink();
+    if (gw != null) {
+      return gw.toRevision(project, c.commit().commit());
+    }
+    return null;
+  }
+
+  private class MyTable extends NavigationTable<ChangeAndCommit> {
+    private final JsArray<ChangeAndCommit> list;
+
+    MyTable(JsArray<ChangeAndCommit> list) {
+      this.list = list;
+      table.setWidth("");
+
+      keysNavigation.setName(Gerrit.C.sectionNavigation());
+      keysNavigation.add(new PrevKeyCommand(0, 'K',
+          Resources.C.previousChange()));
+      keysNavigation.add(new NextKeyCommand(0, 'J', Resources.C.nextChange()));
+      keysNavigation.add(new OpenKeyCommand(0, 'O', Resources.C.openChange()));
+    }
+
+    @Override
+    protected Object getRowItemKey(ChangeAndCommit item) {
+      return item.id();
+    }
+
+    @Override
+    protected ChangeAndCommit getRowItem(int row) {
+      if (0 <= row && row <= list.length()) {
+        return list.get(row);
+      }
+      return null;
+    }
+
+    @Override
+    protected void onOpenRow(int row) {
+      if (0 <= row && row <= list.length()) {
+        ChangeAndCommit c = list.get(row);
+        String url = url(c);
+        if (url != null && url.startsWith("#")) {
+          Gerrit.display(url.substring(1));
+        } else if (url != null) {
+          Window.Location.assign(url);
+        }
+      }
+    }
+
+    void selectRow(int select) {
+      movePointerTo(select, true);
+    }
+  }
+
+  private final class DisplayCommand implements RepeatingCommand {
+    private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
+    private final MyTable table;
+    private final String revision;
+    private final JsArray<ChangeAndCommit> list;
+    private boolean attached;
+    private int row;
+    private int select;
+    private double start;
+
+    private DisplayCommand(String revision, JsArray<ChangeAndCommit> list) {
+      this.table = new MyTable(list);
+      this.revision = revision;
+      this.list = list;
+    }
+
+    public boolean execute() {
+      boolean attachedNow = isAttached();
+      if (!attached && attachedNow) {
+        // Remember that we have been attached at least once. If
+        // later we find we aren't attached we should stop running.
+        attached = true;
+      } else if (attached && !attachedNow) {
+        // If the user navigated away, we aren't in the DOM anymore.
+        // Don't continue to render.
+        return false;
+      }
+
+      start = System.currentTimeMillis();
+      while (row < list.length()) {
+        ChangeAndCommit info = list.get(row);
+        if (revision.equals(info.commit().commit())) {
+          select = row;
+        }
+        render(sb, row, info);
+        if ((++row % 10) == 0 && longRunning()) {
+          updateMeter();
+          return true;
+        }
+      }
+      table.resetHtml(sb);
+      setTable(table);
+      table.selectRow(select);
+      return false;
+    }
+
+    private void render(SafeHtmlBuilder sb, int row, ChangeAndCommit info) {
+      sb.openTr();
+      sb.openTd().setStyleName(FileTable.R.css().pointer()).closeTd();
+
+      sb.openTd().addStyleName(style.subject());
+      String url = url(info);
+      if (url != null) {
+        sb.openAnchor().setAttribute("href", url);
+        if (url.startsWith("#")) {
+          sb.setAttribute("onclick", OPEN + "(event," + row + ")");
+        }
+        sb.append(info.commit().subject());
+        sb.closeAnchor();
+      } else {
+        sb.append(info.commit().subject());
+      }
+      sb.closeTd();
+
+      sb.closeTr();
+    }
+
+    private void updateMeter() {
+      progress.setValue((100 * row) / list.length());
+    }
+
+    private boolean longRunning() {
+      return System.currentTimeMillis() - start > 200;
+    }
+  }
+
+  private static class RelatedInfo extends JavaScriptObject {
+    final native JsArray<ChangeAndCommit> changes() /*-{ return this.changes }-*/;
+    protected RelatedInfo() {
+    }
+  }
+
+  private static class ChangeAndCommit extends JavaScriptObject {
+    final native String id() /*-{ return this.change_id }-*/;
+    final native CommitInfo commit() /*-{ return this.commit }-*/;
+
+    final Change.Id legacy_id() {
+      return has_change_number() ? new Change.Id(_change_number()) : null;
+    }
+
+    final PatchSet.Id patch_set_id() {
+      return has_change_number() && has_revision_number()
+          ? new PatchSet.Id(legacy_id(), _revision_number())
+          : null;
+    }
+
+    private final native boolean has_change_number()
+    /*-{ return this.hasOwnProperty('_change_number') }-*/;
+
+    private final native boolean has_revision_number()
+    /*-{ return this.hasOwnProperty('_revision_number') }-*/;
+
+    private final native int _change_number()
+    /*-{ return this._change_number }-*/;
+
+    private final native int _revision_number()
+    /*-{ return this._revision_number }-*/;
+
+    protected ChangeAndCommit() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml
new file mode 100644
index 0000000..7e02c18
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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.gwtexpui.progress.client'>
+  <ui:style type='com.google.gerrit.client.change.RelatedChanges.Style'>
+    .subject {
+      width: 355px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      display: inline-block;
+    }
+    .header, .subject {
+      white-space: nowrap;
+    }
+  </ui:style>
+
+  <g:HTMLPanel>
+    <div ui:field='header' class='{style.header}'
+         title='Same branch changes connected by Git history'>
+      <ui:attribute name='title'/>
+      <ui:msg>Related Changes</ui:msg>
+    </div>
+    <g:ScrollPanel ui:field='scroll' visible='false'/>
+    <x:ProgressBar ui:field='progress'/>
+    <div ui:field='error' aria-hidden='true' style='display: NONE' class='{style.header}'/>
+    <div ui:field='none' aria-hidden='true' style='display: NONE' class='{style.header}'>
+      <ui:msg>No Related Changes</ui:msg>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java
new file mode 100644
index 0000000..7b88b2b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+
+class Reload extends Image implements ClickHandler,
+    MouseOverHandler, MouseOutHandler {
+  private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+  private Change.Id changeId;
+  private boolean in;
+
+  Reload() {
+    setResource(Resources.I.reload_black());
+    addClickHandler(this);
+    addMouseOverHandler(this);
+    addMouseOutHandler(this);
+  }
+
+  void set(ChangeInfo info) {
+    changeId = info.legacy_id();
+  }
+
+  void reload() {
+    Gerrit.display(PageLinks.toChange(changeId));
+  }
+
+  @Override
+  public void onMouseOver(MouseOverEvent event) {
+    if (!in) {
+      in = true;
+      setResource(Resources.I.reload_white());
+    }
+  }
+
+  @Override
+  public void onMouseOut(MouseOutEvent event) {
+    if (in) {
+      in = false;
+      setResource(Resources.I.reload_black());
+    }
+  }
+
+  @Override
+  public void onClick(ClickEvent e) {
+    if (link.handleAsClick(e.getNativeEvent().<Event> cast())) {
+      e.preventDefault();
+      e.stopPropagation();
+      reload();
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
new file mode 100644
index 0000000..d320df4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JsArrayString;
+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.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class ReplyAction {
+  private final PatchSet.Id psId;
+  private final String revision;
+  private final ChangeScreen2.Style style;
+  private final Widget replyButton;
+
+  private NativeMap<LabelInfo> allLabels;
+  private NativeMap<JsArrayString> permittedLabels;
+
+  private ReplyBox replyBox;
+  private PopupPanel popup;
+
+  ReplyAction(
+      ChangeInfo info,
+      String revision,
+      ChangeScreen2.Style style,
+      Widget replyButton) {
+    this.psId = new PatchSet.Id(
+        info.legacy_id(),
+        info.revisions().get(revision)._number());
+    this.revision = revision;
+    this.style = style;
+    this.replyButton = replyButton;
+
+    boolean current = revision.equals(info.current_revision());
+    allLabels = info.all_labels();
+    permittedLabels = current && info.has_permitted_labels()
+        ? info.permitted_labels()
+        : NativeMap.<JsArrayString> create();
+  }
+
+  void onReply() {
+    if (popup != null) {
+      popup.hide();
+      return;
+    }
+
+    if (replyBox == null) {
+      replyBox = new ReplyBox(
+          psId,
+          revision,
+          allLabels,
+          permittedLabels);
+      allLabels = null;
+      permittedLabels = null;
+    }
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(replyButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(replyBox);
+    p.showRelativeTo(replyButton);
+    GlobalKey.dialog(p);
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
new file mode 100644
index 0000000..c8eb13a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -0,0 +1,285 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+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.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RadioButton;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+class ReplyBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, ReplyBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface Styles extends CssResource {
+    String label_name();
+    String label_value();
+  }
+
+  private final PatchSet.Id psId;
+  private final String revision;
+  private ReviewInput in = ReviewInput.create();
+  private List<Runnable> lgtm;
+
+  @UiField Styles style;
+  @UiField NpTextArea message;
+  @UiField Element labelsParent;
+  @UiField Grid labelsTable;
+  @UiField Button send;
+  @UiField CheckBox email;
+  @UiField Button cancel;
+
+  ReplyBox(
+      PatchSet.Id psId,
+      String revision,
+      NativeMap<LabelInfo> all,
+      NativeMap<JsArrayString> permitted) {
+    this.psId = psId;
+    this.revision = revision;
+    initWidget(uiBinder.createAndBindUi(this));
+
+    List<String> names = new ArrayList<String>(permitted.keySet());
+    if (names.isEmpty()) {
+      UIObject.setVisible(labelsParent, false);
+    } else {
+      Collections.sort(names);
+      lgtm = new ArrayList<Runnable>(names.size());
+      renderLabels(names, all, permitted);
+    }
+  }
+
+  @Override
+  protected void onLoad() {
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        message.setFocus(true);
+      }});
+  }
+
+  @UiHandler("message")
+  void onMessageKey(KeyPressEvent event) {
+    if ((event.getCharCode() == '\n' || event.getCharCode() == KeyCodes.KEY_ENTER)
+        && event.isControlKeyDown()) {
+      event.preventDefault();
+      event.stopPropagation();
+      onSend(null);
+    } else if (lgtm != null
+        && event.getCharCode() == 'M'
+        && message.getValue().equals("LGT")) {
+      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+        @Override
+        public void execute() {
+          if (message.getValue().startsWith("LGTM")) {
+            for (Runnable r : lgtm) {
+              r.run();
+            }
+          }
+        }
+      });
+    }
+  }
+
+  @UiHandler("email")
+  void onEmail(ValueChangeEvent<Boolean> e) {
+    if (e.getValue()) {
+      in.notify(ReviewInput.NotifyHandling.ALL);
+    } else {
+      in.notify(ReviewInput.NotifyHandling.NONE);
+    }
+  }
+
+  @UiHandler("send")
+  void onSend(ClickEvent e) {
+    in.message(message.getText().trim());
+    ChangeApi.revision(psId.getParentKey().get(), revision)
+      .view("review")
+      .post(in, new GerritCallback<ReviewInput>() {
+        @Override
+        public void onSuccess(ReviewInput result) {
+          Gerrit.display(PageLinks.toChange(
+              psId.getParentKey(),
+              String.valueOf(psId.get())));
+        }
+      });
+    hide();
+  }
+
+  @UiHandler("cancel")
+  void onCancel(ClickEvent e) {
+    hide();
+  }
+
+  private void hide() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide();
+        break;
+      }
+    }
+  }
+
+  private void renderLabels(
+      List<String> names,
+      NativeMap<LabelInfo> all,
+      NativeMap<JsArrayString> permitted) {
+    TreeSet<Short> values = new TreeSet<Short>();
+    for (String id : names) {
+      JsArrayString p = permitted.get(id);
+      if (p != null) {
+        for (int i = 0; i < p.length(); i++) {
+          values.add(LabelInfo.parseValue(p.get(i)));
+        }
+      }
+    }
+    List<Short> columns = new ArrayList<Short>(values);
+
+    labelsTable.resize(1 + permitted.size(), 1 + values.size());
+    for (int c = 0; c < columns.size(); c++) {
+      labelsTable.setText(0, 1 + c, LabelValue.formatValue(columns.get(c)));
+      labelsTable.getCellFormatter().setStyleName(0, 1 + c, style.label_value());
+    }
+
+    List<String> checkboxes = new ArrayList<String>(permitted.size());
+    int row = 1;
+    for (String id : names) {
+      Set<Short> vals = all.get(id).value_set();
+      if (isCheckBox(vals)) {
+        checkboxes.add(id);
+      } else {
+        renderRadio(row++, id, columns, vals, all.get(id));
+      }
+    }
+    for (String id : checkboxes) {
+      renderCheckBox(row++, id, all.get(id));
+    }
+  }
+
+  private void renderRadio(int row, final String id,
+      List<Short> columns,
+      Set<Short> values,
+      LabelInfo info) {
+    labelsTable.setText(row, 0, id);
+    labelsTable.getCellFormatter().setStyleName(row, 0, style.label_name());
+
+    ApprovalInfo self = Gerrit.isSignedIn()
+        ? info.for_user(Gerrit.getUserAccount().getId().get())
+        : null;
+
+    final List<RadioButton> group = new ArrayList<RadioButton>(values.size());
+    for (int i = 0; i < columns.size(); i++) {
+      final Short v = columns.get(i);
+      if (values.contains(v)) {
+        RadioButton b = new RadioButton(id);
+        b.setTitle(info.value_text(LabelValue.formatValue(v)));
+        if ((self != null && v == self.value()) || (self == null && v == 0)) {
+          b.setValue(true);
+        }
+        b.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+          @Override
+          public void onValueChange(ValueChangeEvent<Boolean> event) {
+            if (event.getValue()) {
+              in.label(id, v);
+            }
+          }
+        });
+        group.add(b);
+        labelsTable.setWidget(row, 1 + i, b);
+      }
+    }
+
+    if (!group.isEmpty()) {
+      lgtm.add(new Runnable() {
+        @Override
+        public void run() {
+          for (int i = 0; i < group.size() - 1; i++) {
+            group.get(i).setValue(false, false);
+          }
+          group.get(group.size() - 1).setValue(true, true);
+        }
+      });
+    }
+  }
+
+  private void renderCheckBox(int row, final String id, LabelInfo info) {
+    ApprovalInfo self = Gerrit.isSignedIn()
+        ? info.for_user(Gerrit.getUserAccount().getId().get())
+        : null;
+
+    final CheckBox b = new CheckBox();
+    b.setText(id);
+    b.setTitle(info.value_text("+1"));
+    if (self != null && self.value() == 1) {
+      b.setValue(true);
+    }
+    b.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+      @Override
+      public void onValueChange(ValueChangeEvent<Boolean> event) {
+        in.label(id, event.getValue() ? (short) 1 : (short) 0);
+      }
+    });
+    b.setStyleName(style.label_name());
+    labelsTable.setWidget(row, 0, b);
+
+    lgtm.add(new Runnable() {
+      @Override
+      public void run() {
+        b.setValue(true, true);
+      }
+    });
+  }
+
+  private static boolean isCheckBox(Set<Short> values) {
+    return values.size() == 2
+        && values.contains((short) 0)
+        && values.contains((short) 1);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
new file mode 100644
index 0000000..0a7a728
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style type='com.google.gerrit.client.change.ReplyBox.Styles'>
+    .replyBox {
+      max-height: 260px;
+    }
+
+    .label_name {
+      font-weight: bold;
+      text-align: left;
+    }
+    .label_name input { margin-left: 0; }
+
+    .label_value {
+      text-align: center;
+    }
+    .email {
+      display: inline-block;
+      margin-left: 2em;
+    }
+    .cancel {
+      position: absolute;
+      bottom: 5px;
+      right: 5px;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.replyBox}'>
+    <div class='{res.style.section}'>
+      <c:NpTextArea
+         visibleLines='5'
+         characterWidth='70'
+         ui:field='message'/>
+    </div>
+    <div class='{res.style.section}' ui:field='labelsParent'>
+      <g:Grid ui:field='labelsTable'/>
+    </div>
+    <div class='{res.style.section}' style='position: relative'>
+      <g:Button ui:field='send'
+          title='Send reply (Shortcut: Ctrl-Enter)'
+          styleName='{res.style.button}'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Send</ui:msg></div>
+      </g:Button>
+
+      <div class='{style.email}'>
+        <ui:msg>and <g:CheckBox ui:field='email' value='true'>send email</g:CheckBox></ui:msg>
+      </div>
+
+      <g:Button ui:field='cancel'
+          title='Close reply form (Shortcut: Esc)'
+          styleName='{res.style.button}'
+          addStyleNames='{style.cancel}'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Cancel</ui:msg></div>
+      </g:Button>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
new file mode 100644
index 0000000..c1feb5d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.resources.client.ImageResource;
+
+public interface Resources extends ClientBundle {
+  public static final Resources I = GWT.create(Resources.class);
+  static final Constants C = GWT.create(Constants.class);
+
+  @Source("star_open.png") ImageResource star_open();
+  @Source("star_filled.png") ImageResource star_filled();
+  @Source("reload_black.png") ImageResource reload_black();
+  @Source("reload_white.png") ImageResource reload_white();
+  @Source("remove_reviewer.png") ImageResource remove_reviewer();
+  @Source("common.css") Style style();
+
+  public interface Style extends CssResource {
+    String button();
+    String popup();
+    String popupContent();
+    String section();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java
new file mode 100644
index 0000000..7575869
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.admin.Util;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.groups.GroupBaseInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** REST API based suggestion Oracle for reviewers. */
+public class RestReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
+
+  private Change.Id changeId;
+
+  @Override
+  protected void _onRequestSuggestions(final Request req, final Callback callback) {
+    ChangeApi.suggestReviewers(changeId.get(), req.getQuery(),
+        req.getLimit()).get(new GerritCallback<JsArray<SuggestReviewerInfo>>() {
+          @Override
+          public void onSuccess(JsArray<SuggestReviewerInfo> result) {
+            final List<RestReviewerSuggestion> r =
+                new ArrayList<RestReviewerSuggestion>(result.length());
+            for (final SuggestReviewerInfo reviewer : Natives.asList(result)) {
+              r.add(new RestReviewerSuggestion(reviewer));
+            }
+            callback.onSuggestionsReady(req, new Response(r));
+          }
+        });
+  }
+
+  public void setChange(Change.Id changeId) {
+    this.changeId = changeId;
+  }
+
+  private static class RestReviewerSuggestion implements SuggestOracle.Suggestion {
+    private final SuggestReviewerInfo reviewer;
+
+    RestReviewerSuggestion(final SuggestReviewerInfo reviewer) {
+      this.reviewer = reviewer;
+    }
+
+    public String getDisplayString() {
+      if (reviewer.account() != null) {
+        return FormatUtil.nameEmail(reviewer.account());
+      }
+      return reviewer.group().name()
+          + " ("
+          + Util.C.suggestedGroupLabel()
+          + ")";
+    }
+
+    public String getReplacementString() {
+      if (reviewer.account() != null) {
+        return FormatUtil.nameEmail(reviewer.account());
+      }
+      return reviewer.group().name();
+    }
+  }
+
+  public static class SuggestReviewerInfo extends JavaScriptObject {
+    public final native AccountInfo account() /*-{ return this.account; }-*/;
+    public final native GroupBaseInfo group() /*-{ return this.group; }-*/;
+    protected SuggestReviewerInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
new file mode 100644
index 0000000..215f39d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.Button;
+
+class RestoreAction extends ActionMessageBox {
+  private final Change.Id id;
+
+  RestoreAction(Button b, Change.Id id) {
+    super(b);
+    this.id = id;
+  }
+
+  void send(String message) {
+    ChangeApi.restore(id.get(), message, new GerritCallback<ChangeInfo>() {
+      @Override
+      public void onSuccess(ChangeInfo result) {
+        Gerrit.display(PageLinks.toChange(id));
+        hide();
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
new file mode 100644
index 0000000..5f9f076
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.ActionDialog;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.Button;
+
+class RevertAction {
+  static void call(Button b, final Change.Id id, final String revision,
+      String project, final String commitSubject) {
+    // TODO Replace ActionDialog with a nicer looking display.
+    b.setEnabled(false);
+    new ActionDialog(b, false,
+        Util.C.revertChangeTitle(),
+        Util.C.headingRevertMessage()) {
+      {
+        sendButton.setText(Util.C.buttonRevertChangeSend());
+        message.setText(Util.M.revertChangeDefaultMessage(
+            commitSubject, revision));
+      }
+
+      @Override
+      public void onSend() {
+        ChangeApi.revert(id.get(),
+            getMessageText(), new GerritCallback<ChangeInfo>() {
+              @Override
+              public void onSuccess(ChangeInfo result) {
+                sent = true;
+                hide();
+                Gerrit.display(PageLinks.toChange(result.legacy_id()));
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+                enableButtons(true);
+                super.onFailure(caught);
+              }
+            });
+      }
+    }.center();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
new file mode 100644
index 0000000..1902d97
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -0,0 +1,240 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.ConfirmationCallback;
+import com.google.gerrit.client.ConfirmationDialog;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.changes.ApprovalTable.PostInput;
+import com.google.gerrit.client.changes.ApprovalTable.PostResult;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+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.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Add reviewers. */
+class Reviewers extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, Reviewers> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField Element ccText;
+  @UiField Button openForm;
+  @UiField Element form;
+  @UiField Element error;
+  @UiField(provided = true)
+  SuggestBox suggestBox;
+
+  private ChangeScreen2.Style style;
+  private Element reviewersText;
+
+  private RestReviewerSuggestOracle reviewerSuggestOracle;
+  private HintTextBox nameTxtBox;
+  private Change.Id changeId;
+  private boolean submitOnSelection;
+
+  Reviewers() {
+    reviewerSuggestOracle = new RestReviewerSuggestOracle();
+    nameTxtBox = new HintTextBox();
+    suggestBox = new SuggestBox(reviewerSuggestOracle, nameTxtBox);
+    initWidget(uiBinder.createAndBindUi(this));
+
+    nameTxtBox.setVisibleLength(55);
+    nameTxtBox.setHintText(Util.C.approvalTableAddReviewerHint());
+    nameTxtBox.addKeyDownHandler(new KeyDownHandler() {
+      @Override
+      public void onKeyDown(KeyDownEvent e) {
+        submitOnSelection = false;
+
+        if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+          onCancel(null);
+        } else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          if (((DefaultSuggestionDisplay) suggestBox.getSuggestionDisplay())
+              .isSuggestionListShowing()) {
+            submitOnSelection = true;
+          } else {
+            onAdd(null);
+          }
+        }
+      }
+    });
+    suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
+      @Override
+      public void onSelection(SelectionEvent<Suggestion> event) {
+        nameTxtBox.setFocus(true);
+        if (submitOnSelection) {
+          onAdd(null);
+        }
+      }
+    });
+  }
+
+  void init(ChangeScreen2.Style style, Element reviewersText) {
+    this.style = style;
+    this.reviewersText = reviewersText;
+  }
+
+  void set(ChangeInfo info) {
+    this.changeId = info.legacy_id();
+    display(info);
+    reviewerSuggestOracle.setChange(changeId);
+    openForm.setVisible(Gerrit.isSignedIn());
+  }
+
+  @UiHandler("openForm")
+  void onOpenForm(ClickEvent e) {
+    onOpenForm();
+  }
+
+  void onOpenForm() {
+    UIObject.setVisible(form, true);
+    UIObject.setVisible(error, false);
+    openForm.setVisible(false);
+    suggestBox.setFocus(true);
+  }
+
+  @UiHandler("add")
+  void onAdd(ClickEvent e) {
+    String reviewer = suggestBox.getText();
+    if (!reviewer.isEmpty()) {
+      addReviewer(reviewer, false);
+    }
+  }
+
+  @UiHandler("cancel")
+  void onCancel(ClickEvent e) {
+    openForm.setVisible(true);
+    UIObject.setVisible(form, false);
+    suggestBox.setFocus(false);
+  }
+
+  private void addReviewer(final String reviewer, boolean confirmed) {
+    ChangeApi.reviewers(changeId.get()).post(
+        PostInput.create(reviewer, confirmed),
+        new GerritCallback<PostResult>() {
+          public void onSuccess(PostResult result) {
+            nameTxtBox.setEnabled(true);
+
+            if (result.confirm()) {
+              askForConfirmation(result.error());
+            } else if (result.error() != null) {
+              UIObject.setVisible(error, true);
+              error.setInnerText(result.error());
+            } else {
+              UIObject.setVisible(error, false);
+              error.setInnerText("");
+              nameTxtBox.setText("");
+
+              if (result.reviewers() != null
+                  && result.reviewers().length() > 0) {
+                updateReviewerList();
+              }
+            }
+          }
+
+          private void askForConfirmation(String text) {
+            new ConfirmationDialog(
+                Util.C.approvalTableAddManyReviewersConfirmationDialogTitle(),
+                new SafeHtmlBuilder().append(text),
+                new ConfirmationCallback() {
+                  @Override
+                  public void onOk() {
+                    addReviewer(reviewer, true);
+                  }
+                }).center();
+          }
+
+          @Override
+          public void onFailure(Throwable err) {
+            UIObject.setVisible(error, true);
+            error.setInnerText(err instanceof StatusCodeException
+                ? ((StatusCodeException) err).getEncodedResponse()
+                : err.getMessage());
+            nameTxtBox.setEnabled(true);
+          }
+        });
+  }
+
+  private void updateReviewerList() {
+    ChangeApi.detail(changeId.get(),
+        new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            display(result);
+          }
+        });
+  }
+
+  private void display(ChangeInfo info) {
+    Map<Integer, AccountInfo> r = new HashMap<Integer, AccountInfo>();
+    Map<Integer, AccountInfo> cc = new HashMap<Integer, AccountInfo>();
+    for (LabelInfo label : Natives.asList(info.all_labels().values())) {
+      if (label.all() != null) {
+        for (ApprovalInfo ai : Natives.asList(label.all())) {
+          (ai.value() != 0 ? r : cc).put(ai._account_id(), ai);
+        }
+      }
+    }
+    for (Integer i : r.keySet()) {
+      cc.remove(i);
+    }
+    r.remove(info.owner()._account_id());
+    cc.remove(info.owner()._account_id());
+
+    Set<Integer> removable = new HashSet<Integer>();
+    if (info.removable_reviewers() != null) {
+      for (AccountInfo a : Natives.asList(info.removable_reviewers())) {
+        removable.add(a._account_id());
+      }
+    }
+
+    SafeHtml rHtml = Labels.formatUserList(style, r.values(), removable);
+    SafeHtml ccHtml = Labels.formatUserList(style, cc.values(), removable);
+
+    reviewersText.setInnerSafeHtml(rHtml);
+    ccText.setInnerSafeHtml(ccHtml);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
new file mode 100644
index 0000000..9aed0d7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style>
+    .openAdd {
+      cursor: pointer;
+      float: right;
+      padding: 0;
+      margin: 0;
+      border: 0;
+      background-color: transparent;
+    }
+
+    .suggestBox {
+      margin-bottom: 2px;
+    }
+
+    .error {
+      color: #D33D3D;
+      font-weight: bold;
+    }
+
+    .cancel {
+      float: right;
+    }
+  </ui:style>
+  <g:HTMLPanel>
+    <div>
+      <span ui:field='ccText'/>
+      <g:Button ui:field='openForm'
+         title='Add reviewers to this change'
+         styleName='{style.openAdd}'
+         visible='false'>
+       <ui:attribute name='title'/>
+       <div>[+]</div>
+      </g:Button>
+    </div>
+    <div ui:field='form' style='display: none' aria-hidden='true'>
+      <g:SuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
+      <div ui:field='error'
+           class='{style.error}'
+           style='display: none' aria-hidden='true'/>
+      <div>
+        <g:Button ui:field='add' styleName='{res.style.button}'>
+          <div>Add</div>
+        </g:Button>
+        <g:Button ui:field='cancel'
+            styleName='{res.style.button}'
+            addStyleNames='{style.cancel}'>
+          <div>Cancel</div>
+        </g:Button>
+      </div>
+    </div>
+   </g:HTMLPanel>
+  </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsAction.java
new file mode 100644
index 0000000..d66127b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsAction.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+class RevisionsAction extends RightSidePopdownAction {
+  private final RevisionsBox revisionBox;
+
+  RevisionsAction(
+      Change.Id changeId,
+      String revision,
+      ChangeScreen2.Style style,
+      UIObject relativeTo,
+      Widget downloadButton) {
+    super(style, relativeTo, downloadButton);
+    this.revisionBox = new RevisionsBox(changeId, revision);
+  }
+
+  Widget getWidget() {
+    return revisionBox;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java
new file mode 100644
index 0000000..6dac1e9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.ui.FancyFlexTableImpl;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.dom.client.NativeEvent;
+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.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import java.util.Collections;
+import java.util.EnumSet;
+
+class RevisionsBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, RevisionsBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private static final String OPEN;
+  private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+
+  static {
+    OPEN = DOM.createUniqueId().replace('-', '_');
+    init(OPEN);
+  }
+
+  private static final native void init(String o) /*-{
+    $wnd[o] = $entry(function(e,i) {
+      return @com.google.gerrit.client.change.RevisionsBox::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
+    });
+  }-*/;
+
+  private static boolean onOpen(NativeEvent e, int idx) {
+    if (link.handleAsClick(e.<Event> cast())) {
+      RevisionsBox t = getRevisionBox(e);
+      if (t != null) {
+        t.onOpenRow(idx);
+        e.preventDefault();
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static RevisionsBox getRevisionBox(NativeEvent event) {
+    com.google.gwt.user.client.Element e = event.getEventTarget().cast();
+    for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+      EventListener l = DOM.getEventListener(e);
+      if (l instanceof RevisionsBox) {
+        return (RevisionsBox) l;
+      }
+    }
+    return null;
+  }
+
+  interface Style extends CssResource {
+    String current();
+    String legacy_id();
+    String commit();
+    String draft_comment();
+  }
+
+  private final Change.Id changeId;
+  private final String revision;
+  private boolean loaded;
+  private JsArray<RevisionInfo> revisions;
+
+  @UiField FlexTable table;
+  @UiField Style style;
+
+  RevisionsBox(Change.Id changeId, String revision) {
+    this.changeId = changeId;
+    this.revision = revision;
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  @Override
+  protected void onLoad() {
+    if (!loaded) {
+      RestApi call = ChangeApi.detail(changeId.get());
+      ChangeList.addOptions(call, EnumSet.of(
+          ListChangesOption.ALL_COMMITS,
+          ListChangesOption.ALL_REVISIONS,
+          ListChangesOption.DRAFT_COMMENTS));
+      call.get(new AsyncCallback<ChangeInfo>() {
+        @Override
+        public void onSuccess(ChangeInfo result) {
+          render(result.revisions());
+          loaded = true;
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      });
+    }
+  }
+
+  private void onOpenRow(int idx) {
+    closeParent();
+    Gerrit.display(url(revisions.get(idx)));
+  }
+
+  private void render(NativeMap<RevisionInfo> map) {
+    map.copyKeysIntoChildren("name");
+
+    revisions = map.values();
+    RevisionInfo.sortRevisionInfoByNumber(revisions);
+    Collections.reverse(Natives.asList(revisions));
+
+    SafeHtmlBuilder sb = new SafeHtmlBuilder();
+    header(sb);
+    for (int i = 0; i < revisions.length(); i++) {
+      revision(sb, i, revisions.get(i));
+    }
+
+    GWT.<FancyFlexTableImpl> create(FancyFlexTableImpl.class)
+      .resetHtml(table, sb);
+  }
+
+  private void header(SafeHtmlBuilder sb) {
+    sb.openTr()
+      .openTh()
+          .setStyleName(style.legacy_id())
+          .append(Resources.C.ps())
+          .closeTh()
+      .openTh().append(Resources.C.commit()).closeTh()
+      .openTh().append(Resources.C.date()).closeTh()
+      .openTh().append(Resources.C.author()).closeTh()
+      .closeTr();
+  }
+
+  private void revision(SafeHtmlBuilder sb, int index, RevisionInfo r) {
+    CommitInfo c = r.commit();
+    sb.openTr();
+    if (revision.equals(r.name())) {
+      sb.setStyleName(style.current());
+    }
+
+    sb.openTd().setStyleName(style.legacy_id());
+    if (r.draft()) {
+      sb.append(Resources.C.draft()).append(' ');
+    }
+    if (r.has_draft_comments()) {
+      sb.openSpan()
+        .addStyleName(style.draft_comment())
+        .setAttribute("title", Resources.C.draftCommentsTooltip())
+        .append(new ImageResourceRenderer()
+            .render(Gerrit.RESOURCES.draftComments()))
+        .closeSpan()
+        .append(' ');
+    }
+    sb.append(r._number());
+    sb.closeTd();
+
+    sb.openTd()
+      .setStyleName(style.commit())
+      .openAnchor()
+      .setAttribute("href", "#" + url(r))
+      .setAttribute("onclick", OPEN + "(event," + index + ")")
+      .append(r.name().substring(0, 10))
+      .closeAnchor()
+      .closeTd();
+
+    sb.openTd()
+      .append(FormatUtil.shortFormatDayTime(c.committer().date()))
+      .closeTd();
+
+    String an = c.author() != null ? c.author().name() : null;
+    String cn = c.committer() != null ? c.committer().name() : null;
+    sb.openTd();
+    sb.append(an);
+    if (!"".equals(an) && !"".equals(cn) && !an.equals(cn)) {
+      sb.append(" / ").append(cn);
+    }
+    sb.closeTd();
+
+    sb.closeTr();
+  }
+
+  private String url(RevisionInfo r) {
+    return PageLinks.toChange(
+        changeId,
+        String.valueOf(r._number()));
+  }
+
+  private void closeParent() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide(true);
+        break;
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.ui.xml
new file mode 100644
index 0000000..d7a8fc4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.ui.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style type='com.google.gerrit.client.change.RevisionsBox.Style'>
+    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+
+    .revisionBox {
+      min-width: 300px;
+      margin: 10px 0px 5px 5px;
+    }
+
+    .scroll {
+      min-width: 300px;
+      height: 200px;
+    }
+
+    .table {
+      border-spacing: 0;
+      width: 100%;
+    }
+
+    .table td, .table th {
+      padding-left: 5px;
+      padding-right: 5px;
+      border-right: 2px solid #ddd;
+      white-space: nowrap;
+    }
+
+    .table tr.current {
+      background-color: selectionColor;
+    }
+    .table tr.current a {
+      pointer-events: none;
+      color: #000;
+    }
+
+    .legacy_id {
+      min-width: 50px;
+      text-align: right;
+      font-weight: bold;
+    }
+
+    .commit {
+      font-family: monospace;
+    }
+
+    .draft_comment {
+      margin: 0 2px 0 0;
+      width: 16px;
+      height: 16px;
+      vertical-align: bottom;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.revisionBox}'>
+    <g:ScrollPanel styleName='{style.scroll}'>
+      <g:FlexTable ui:field='table' styleName='{style.table}'/>
+    </g:ScrollPanel>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
new file mode 100644
index 0000000..3424064
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+abstract class RightSidePopdownAction {
+  private final ChangeScreen2.Style style;
+  private final Widget button;
+  private final UIObject relativeTo;
+  private PopupPanel popup;
+
+  RightSidePopdownAction(
+      ChangeScreen2.Style style,
+      UIObject relativeTo,
+      Widget button) {
+    this.style = style;
+    this.relativeTo = relativeTo;
+    this.button = button;
+  }
+
+  abstract Widget getWidget();
+
+  void show() {
+    if (popup != null) {
+      button.removeStyleName(style.selected());
+      popup.hide();
+      return;
+    }
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true) {
+      @Override
+      public void setPopupPosition(int left, int top) {
+        top -= Document.get().getBodyOffsetTop();
+
+        int w = Window.getScrollLeft() + Window.getClientWidth();
+        int r = relativeTo.getAbsoluteLeft() + relativeTo.getOffsetWidth();
+        int right = w - r;
+        Style style = getElement().getStyle();
+        style.clearProperty("left");
+        style.setPropertyPx("right", right);
+        style.setPropertyPx("top", top);
+      }
+    };
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(button.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          button.removeStyleName(style.selected());
+          popup = null;
+        }
+      }
+    });
+    p.add(getWidget());
+    p.showRelativeTo(relativeTo);
+    GlobalKey.dialog(p);
+    button.addStyleName(style.selected());
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
new file mode 100644
index 0000000..fccca27
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
@@ -0,0 +1,26 @@
+// 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.client.change;
+
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.ToggleButton;
+
+class StarIcon extends ToggleButton {
+  StarIcon() {
+    super(
+      new Image(Resources.I.star_open()),
+      new Image(Resources.I.star_filled()));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
new file mode 100644
index 0000000..8a3aae5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.SubmitFailureDialog;
+import com.google.gerrit.client.changes.SubmitInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+
+class SubmitAction {
+  static void call(final Change.Id id, String revision) {
+    ChangeApi.submit(id.get(), revision,
+      new GerritCallback<SubmitInfo>() {
+        public void onSuccess(SubmitInfo result) {
+          redisplay();
+        }
+
+        public void onFailure(Throwable err) {
+          if (SubmitFailureDialog.isConflict(err)) {
+            new SubmitFailureDialog(err.getMessage()).center();
+            redisplay();
+          } else {
+            super.onFailure(err);
+          }
+        }
+
+        private void redisplay() {
+          Gerrit.display(PageLinks.toChange(id));
+        }
+      });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
new file mode 100644
index 0000000..4f98bdb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+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.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+/** Displays (and edits) the change topic string. */
+class Topic extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, Topic> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private PatchSet.Id psId;
+  private boolean canEdit;
+
+  @UiField FlowPanel show;
+  @UiField InlineLabel text;
+  @UiField Image editIcon;
+
+  @UiField Element form;
+  @UiField NpTextBox input;
+  @UiField NpTextArea message;
+  @UiField Button save;
+  @UiField Button cancel;
+
+  Topic() {
+    initWidget(uiBinder.createAndBindUi(this));
+    show.addDomHandler(
+      new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent event) {
+          onEdit();
+        }
+      },
+      ClickEvent.getType());
+  }
+
+  void set(ChangeInfo info, String revision) {
+    canEdit = info.has_actions()
+        && info.actions().containsKey("topic")
+        && info.actions().get("topic").enabled();
+
+    psId = new PatchSet.Id(
+        info.legacy_id(),
+        info.revisions().get(revision)._number());
+
+    text.setText(info.topic());
+    editIcon.setVisible(canEdit);
+    if (!canEdit) {
+      show.setTitle(null);
+    }
+  }
+
+  boolean canEdit() {
+    return canEdit;
+  }
+
+  void onEdit() {
+    if (canEdit) {
+      show.setVisible(false);
+      UIObject.setVisible(form, true);
+
+      input.setText(text.getText());
+      input.setFocus(true);
+    }
+  }
+
+  @UiHandler("cancel")
+  void onCancel(ClickEvent e) {
+    input.setFocus(false);
+    show.setVisible(true);
+    UIObject.setVisible(form, false);
+  }
+
+  @UiHandler("input")
+  void onKeyDownInput(KeyDownEvent e) {
+    if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+      onCancel(null);
+    } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+      onSave(null);
+    }
+  }
+
+  @UiHandler("message")
+  void onKeyDownMessage(KeyDownEvent e) {
+    if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+      onCancel(null);
+    } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER
+        && e.isControlKeyDown()) {
+      onSave(null);
+    }
+  }
+
+  @UiHandler("save")
+  void onSave(ClickEvent e) {
+    ChangeApi.topic(
+        psId.getParentKey().get(),
+        input.getValue().trim(),
+        message.getValue().trim(),
+        new GerritCallback<String>() {
+          @Override
+          public void onSuccess(String result) {
+            Gerrit.display(PageLinks.toChange(
+                psId.getParentKey(),
+                String.valueOf(psId.get())));
+          }
+        });
+    onCancel(null);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
new file mode 100644
index 0000000..2f4751c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style>
+    .show { cursor: pointer; }
+    .edit, .cancel { float: right; }
+  </ui:style>
+  <g:HTMLPanel>
+    <g:FlowPanel ui:field='show'
+        styleName='{style.show}'
+        title='Click to edit topic (Shortcut: t)'>
+      <ui:attribute name='title'/>
+      <g:InlineLabel ui:field='text'/>
+      <g:Image ui:field='editIcon'
+          resource='{ico.edit}'
+          styleName='{style.edit}'/>
+    </g:FlowPanel>
+
+    <div ui:field='form' style='display: none' aria-hidden='true'>
+      <div>
+        <c:NpTextBox ui:field='input' visibleLength='55'/>
+      </div>
+      <div>
+        <c:NpTextArea ui:field='message'
+            visibleLines='3'
+            characterWidth='45'/>
+      </div>
+      <div>
+        <g:Button ui:field='save' styleName='{res.style.button}'>
+          <div>Update</div>
+        </g:Button>
+        <g:Button ui:field='cancel'
+            styleName='{res.style.button}'
+            addStyleNames='{style.cancel}'>
+          <div>Cancel</div>
+        </g:Button>
+      </div>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
new file mode 100644
index 0000000..1dece8b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+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.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+import java.sql.Timestamp;
+import java.util.HashSet;
+import java.util.List;
+
+/** Displays the "New Message From ..." panel in bottom right on updates. */
+abstract class UpdateAvailableBar extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, UpdateAvailableBar> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private Timestamp updated;
+
+  @UiField Element author;
+  @UiField Anchor show;
+  @UiField Anchor ignore;
+
+  UpdateAvailableBar() {
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void set(List<MessageInfo> newMessages, Timestamp newTime) {
+    HashSet<Integer> seen = new HashSet<Integer>();
+    StringBuilder r = new StringBuilder();
+    for (MessageInfo m : newMessages) {
+      int a = m.author() != null ? m.author()._account_id() : 0;
+      if (seen.add(a)) {
+        if (r.length() > 0) {
+          r.append(", ");
+        }
+        r.append(Message.authorName(m));
+      }
+    }
+    author.setInnerText(r.toString());
+    updated = newTime;
+  }
+
+  @UiHandler("show")
+  void onShow(ClickEvent e) {
+    onShow();
+  }
+
+  @UiHandler("ignore")
+  void onIgnore(ClickEvent e) {
+    onIgnore(updated);
+    removeFromParent();
+  }
+
+  abstract void onShow();
+  abstract void onIgnore(Timestamp newTime);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
new file mode 100644
index 0000000..1c46b8c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:style>
+    .popup {
+      position: fixed;
+      bottom: 0;
+      right: 0;
+      z-index: 10;
+      padding: 5px;
+    }
+    .bar {
+      background-color: #fff1a8;
+      border: 1px solid #ccc;
+      padding: 5px 10px;
+      font-size: 80%;
+      color: #222;
+      white-space: nowrap;
+      width: auto;
+      height: auto;
+    }
+    a.action {
+      color: #222;
+      text-decoration: underline;
+      display: inline-block;
+      margin-left: 0.5em;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.popup}'>
+    <div class='{style.bar}'>
+      <ui:msg>Update from <span ui:field='author'/></ui:msg>
+      <g:Anchor ui:field='show'
+          styleName='{style.action}'
+          href='javascript:;'
+          title='Refresh screen and display updates'>
+        <ui:attribute name='title'/>
+        <ui:msg>Show</ui:msg>
+      </g:Anchor>
+      <g:Anchor ui:field='ignore'
+          styleName='{style.action}'
+          href='javascript:;'
+          title='Ignore this update'>
+        <ui:attribute name='title'/>
+        <ui:msg>Ignore</ui:msg>
+      </g:Anchor>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
new file mode 100644
index 0000000..9d2a467
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.ui.UserActivityMonitor;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+class UpdateCheckTimer extends Timer implements ValueChangeHandler<Boolean> {
+  private static final int MAX_PERIOD = 3 * 60 * 1000;
+  private static final int IDLE_PERIOD = 2 * 3600 * 1000;
+  private static final int POLL_PERIOD =
+      Gerrit.getConfig().getChangeUpdateDelay() * 1000;
+
+  private final ChangeScreen2 screen;
+  private int delay;
+  private boolean running;
+
+  UpdateCheckTimer(ChangeScreen2 screen) {
+    this.screen = screen;
+    this.delay = POLL_PERIOD;
+  }
+
+  void schedule() {
+    scheduleRepeating(delay);
+  }
+
+  @Override
+  public void run() {
+    if (!screen.isAttached()) {
+      // screen should have cancelled this timer.
+      cancel();
+      return;
+    } else if (running) {
+      return;
+    }
+
+    running = true;
+    screen.loadChangeInfo(false, new AsyncCallback<ChangeInfo>() {
+      @Override
+      public void onSuccess(ChangeInfo info) {
+        running = false;
+        screen.showUpdates(info);
+
+        int d = UserActivityMonitor.isActive()
+            ? POLL_PERIOD
+            : IDLE_PERIOD;
+        if (d != delay) {
+          delay = d;
+          schedule();
+        }
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        // On failures increase the delay time and try again,
+        // but place an upper bound on the delay.
+        running = false;
+        delay = (int) Math.max(
+            delay * (1.5 + Math.random()),
+            UserActivityMonitor.isActive()
+              ? MAX_PERIOD
+              : IDLE_PERIOD + MAX_PERIOD);
+        schedule();
+      }
+    });
+  }
+
+  @Override
+  public void onValueChange(ValueChangeEvent<Boolean> event) {
+    if (event.getValue()) {
+      delay = POLL_PERIOD;
+      run();
+    } else {
+      delay = IDLE_PERIOD;
+    }
+    schedule();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
new file mode 100644
index 0000000..ae00dc7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
@@ -0,0 +1,63 @@
+/* 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.
+ */
+
+@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+.popup {
+  background-color: trimColor;
+  min-width: 300px;
+  min-height: 90px;
+}
+
+.popupContent {
+  padding: 5px;
+}
+
+.button,
+.popup button,
+.popup input[type='button'] {
+  margin: 0 3px 0 0;
+  border-color: rgba(0, 0, 0, 0.1);
+  text-align: center;
+  font-size: 11px;
+  font-weight: bold;
+  border: 1px solid;
+  cursor: pointer;
+  color: #fff;
+  background-color: #4d90fe;
+  background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+  -webkit-border-radius: 2px;
+  -webkit-box-sizing: content-box;
+}
+
+.button:disabled,
+.popup button:disabled,
+.popup input[type='button']:disabled {
+  background-color: #999;
+  background-image: -webkit-linear-gradient(top, #999, #999);
+}
+
+.button div, .popup button div {
+  width: 54px;
+  white-space: nowrap;
+  color: #fff;
+  height: 10px;
+  line-height: 10px;
+}
+
+.section {
+  padding: 5px 5px;
+  border-bottom: 1px solid #b8b8b8;
+}
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
new file mode 100644
index 0000000..a9d5a38
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -0,0 +1,73 @@
+/* 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.
+ */
+
+.pointer, .reviewed {
+  width: 12px;
+  padding: 0px;
+  vertical-align: top;
+}
+
+.status {
+  padding-right: 4px;
+  color: #888;
+}
+
+.pathColumn {
+  white-space: nowrap;
+  min-width: 600px;
+}
+.pathColumn a {
+  color: #000;
+}
+.commonPrefix {
+  color: #888;
+}
+
+.draftColumn,
+.newColumn,
+.commentColumn {
+  white-space: nowrap;
+}
+.draftColumn {
+  color: #d44;
+  font-weight: bold;
+}
+.newColumn {
+  font-weight: bold;
+}
+
+.deltaColumn1 {
+  white-space: nowrap;
+  text-align: right;
+}
+
+.deltaColumn2 {
+  padding-left: 5px;
+  white-space: nowrap;
+  text-align: right;
+}
+
+.inserted {
+  height: 10px;
+  display: inline-block;
+  background-color: #4d4;
+}
+
+.deleted {
+  height: 10px;
+  display: inline-block;
+  background-color: #d44;
+}
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_black.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_black.png
new file mode 100644
index 0000000..13f3a5d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_black.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_white.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_white.png
new file mode 100644
index 0000000..8f5a311
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_white.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/remove_reviewer.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/remove_reviewer.png
new file mode 100644
index 0000000..9e494dd
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/remove_reviewer.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png
new file mode 100644
index 0000000..39bddb1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png
new file mode 100644
index 0000000..6c955de
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index 2580542..5f89bf0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -19,11 +19,15 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.changes.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.EnumSet;
 
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
   private final Account.Id ownerId;
@@ -41,7 +45,16 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    table = new ChangeTable2();
+    table = new ChangeTable2() {
+      {
+        keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
+          @Override
+          public void onKeyPress(final KeyPressEvent event) {
+            Gerrit.display(getToken());
+          }
+        });
+      }
+    };
     table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
 
     outgoing = new ChangeTable2.Section();
@@ -50,7 +63,7 @@
 
     outgoing.setTitleText(Util.C.outgoingReviews());
     incoming.setTitleText(Util.C.incomingReviews());
-    incoming.setHighlightUnreviewed(true);
+    incoming.setHighlightUnreviewed(mine);
     closed.setTitleText(Util.C.recentlyClosed());
 
     table.addSection(outgoing);
@@ -63,6 +76,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+
     String who = mine ? "self" : ownerId.toString();
     ChangeList.query(
         new ScreenLoadCallback<JsArray<ChangeList>>(this) {
@@ -71,6 +85,9 @@
             display(result);
           }
         },
+        mine
+          ? EnumSet.of(ListChangesOption.REVIEWED)
+          : EnumSet.noneOf(ListChangesOption.class),
         "is:open owner:" + who,
         "is:open reviewer:" + who + " -owner:" + who,
         "is:closed owner:" + who + " -age:4w limit:10");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index 08877d9..404de96 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -52,6 +52,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -105,6 +106,13 @@
     setStyleName(Gerrit.RESOURCES.css().approvalTable());
   }
 
+  /**
+   * Sets the header row
+   *
+   * @param labels The list of labels to display in the header. This list does
+   *    not get resorted, so be sure that the list's elements are in the same
+   *    order as the list of labels passed to the {@code displayRow} method.
+   */
   private void displayHeader(Collection<String> labels) {
     table.resizeColumns(2 + labels.size());
 
@@ -144,12 +152,14 @@
     if (byUser.isEmpty()) {
       table.setVisible(false);
     } else {
-      displayHeader(change.labels());
+      List<String> labels = new ArrayList<String>(change.labels());
+      Collections.sort(labels);
+      displayHeader(labels);
       table.resizeRows(1 + byUser.size());
       int i = 1;
       for (ApprovalDetail ad : ApprovalDetail.sort(
           byUser.values(), change.owner()._account_id())) {
-        displayRow(i++, ad, change, accounts.get(ad.getAccount().get()));
+        displayRow(i++, ad, labels, accounts.get(ad.getAccount().get()));
       }
       table.setVisible(true);
     }
@@ -165,14 +175,14 @@
 
   private void removeAllChildren(Element el) {
     for (int i = DOM.getChildCount(el) - 1; i >= 0; i--) {
-      DOM.removeChild(el, DOM.getChild(el, i));
+      el.removeChild(DOM.getChild(el, i));
     }
   }
 
   private void addMissingLabel(String text) {
     Element li = DOM.createElement("li");
     li.setClassName(Gerrit.RESOURCES.css().missingApproval());
-    DOM.setInnerText(li, text);
+    li.setInnerText(text);
     DOM.appendChild(missing.getElement(), li);
   }
 
@@ -244,8 +254,8 @@
     }
   }
 
-  private static class PostInput extends JavaScriptObject {
-    static PostInput create(String reviewer, boolean confirmed) {
+  public static class PostInput extends JavaScriptObject {
+    public static PostInput create(String reviewer, boolean confirmed) {
       PostInput input = createObject().cast();
       input.init(reviewer, confirmed);
       return input;
@@ -262,7 +272,7 @@
     }
   }
 
-  private static class ReviewerInfo extends AccountInfo {
+  public static class ReviewerInfo extends AccountInfo {
     final Set<String> approvals() {
       return Natives.keys(_approvals());
     }
@@ -273,10 +283,10 @@
     }
   }
 
-  private static class PostResult extends JavaScriptObject {
-    final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
-    final native boolean confirm() /*-{ return this.confirm || false; }-*/;
-    final native String error() /*-{ return this.error; }-*/;
+  public static class PostResult extends JavaScriptObject {
+    public final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
+    public final native boolean confirm() /*-{ return this.confirm || false; }-*/;
+    public final native String error() /*-{ return this.error; }-*/;
 
     protected PostResult() {
     }
@@ -324,8 +334,18 @@
         });
   }
 
-  private void displayRow(int row, final ApprovalDetail ad, ChangeInfo change,
-      AccountInfo account) {
+  /**
+   * Sets the reviewer data for a row.
+   *
+   * @param row The number of the row on which to set the reviewer.
+   * @param ad The details for this reviewer's approval.
+   * @param labels The list of labels to show. This list does not get resorted,
+   *    so be sure that the list's elements are in the same order as the list
+   *    of labels passed to the {@code displayHeader} method.
+   * @param account The account information for the approval.
+   */
+  private void displayRow(int row, final ApprovalDetail ad,
+      List<String> labels, AccountInfo account) {
     final CellFormatter fmt = table.getCellFormatter();
     int col = 0;
 
@@ -351,7 +371,7 @@
     }
     fmt.setStyleName(row, col++, Gerrit.RESOURCES.css().removeReviewerCell());
 
-    for (String labelName : change.labels()) {
+    for (String labelName : labels) {
       fmt.setStyleName(row, col, Gerrit.RESOURCES.css().approvalscore());
       if (!ad.canVote(labelName)) {
         fmt.addStyleName(row, col, Gerrit.RESOURCES.css().notVotable());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index abd94c9..97b0166 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.changes.ChangeInfo.IncludedInInfo;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -62,17 +63,40 @@
   }
 
   public static void detail(int id, AsyncCallback<ChangeInfo> cb) {
-    call(id, "detail").get(cb);
+    detail(id).get(cb);
+  }
+
+  public static RestApi detail(int id) {
+    return call(id, "detail");
+  }
+
+  public static void includedIn(int id, AsyncCallback<IncludedInInfo> cb) {
+    call(id, "in").get(cb);
+  }
+
+  public static RestApi revision(int id, String revision) {
+    return change(id).view("revisions").id(revision);
   }
 
   public static RestApi revision(PatchSet.Id id) {
-    return change(id.getParentKey().get()).view("revisions").id(id.get());
+    int cn = id.getParentKey().get();
+    String revision = RevisionInfoCache.get(id);
+    if (revision != null) {
+      return revision(cn, revision);
+    }
+    return change(cn).view("revisions").id(id.get());
   }
 
   public static RestApi reviewers(int id) {
     return change(id).view("reviewers");
   }
 
+  public static RestApi suggestReviewers(int id, String q, int n) {
+    return change(id).view("suggest_reviewers")
+        .addParameter("q", q)
+        .addParameter("n", n);
+  }
+
   public static RestApi reviewer(int id, int reviewer) {
     return change(id).view("reviewers").id(reviewer);
   }
@@ -82,12 +106,50 @@
   }
 
   /** Submit a specific revision of a change. */
+  public static void cherrypick(int id, String commit, String destination, String message, AsyncCallback<ChangeInfo> cb) {
+    CherryPickInput cherryPickInput = CherryPickInput.create();
+    cherryPickInput.setMessage(message);
+    cherryPickInput.setDestination(destination);
+    call(id, commit, "cherrypick").post(cherryPickInput, cb);
+  }
+
+  /** Edit commit message for specific revision of a change. */
+  public static void message(int id, String commit, String message,
+      AsyncCallback<JavaScriptObject> cb) {
+    CherryPickInput input = CherryPickInput.create();
+    input.setMessage(message);
+    call(id, commit, "message").post(input, cb);
+  }
+
+  /** Submit a specific revision of a change. */
   public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
     SubmitInput in = SubmitInput.create();
     in.wait_for_merge(true);
     call(id, commit, "submit").post(in, cb);
   }
 
+  /** Publish a specific revision of a draft change. */
+  public static void publish(int id, String commit, AsyncCallback<JavaScriptObject> cb) {
+    JavaScriptObject in = JavaScriptObject.createObject();
+    call(id, commit, "publish").post(in, cb);
+  }
+
+  /** Delete a specific draft change. */
+  public static void deleteChange(int id, AsyncCallback<JavaScriptObject> cb) {
+    change(id).delete(cb);
+  }
+
+  /** Delete a specific draft patch set. */
+  public static void deleteRevision(int id, String commit, AsyncCallback<JavaScriptObject> cb) {
+    revision(id, commit).delete(cb);
+  }
+
+  /** Rebase a revision onto the branch tip. */
+  public static void rebase(int id, String commit, AsyncCallback<ChangeInfo> cb) {
+    JavaScriptObject in = JavaScriptObject.createObject();
+    call(id, commit, "rebase").post(in, cb);
+  }
+
   private static class Input extends JavaScriptObject {
     final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
     final native void message(String m) /*-{ if(m)this.message=m; }-*/;
@@ -100,6 +162,17 @@
     }
   }
 
+  private static class CherryPickInput extends JavaScriptObject {
+    static CherryPickInput create() {
+      return (CherryPickInput) createObject();
+    }
+    final native void setDestination(String d) /*-{ this.destination = d; }-*/;
+    final native void setMessage(String m) /*-{ this.message = m; }-*/;
+
+    protected CherryPickInput() {
+    }
+  };
+
   private static class SubmitInput extends JavaScriptObject {
     final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
 
@@ -119,7 +192,7 @@
     return change(id).view("revisions").id(commit).view(action);
   }
 
-  private static RestApi change(int id) {
+  public static RestApi change(int id) {
     // TODO Switch to triplet project~branch~id format in URI.
     return new RestApi("/changes/").id(String.valueOf(id));
   }
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 968c726..2d95915 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -22,6 +22,9 @@
   String statusLongMerged();
   String statusLongAbandoned();
   String statusLongDraft();
+  String readyToSubmit();
+  String mergeConflict();
+  String notCurrent();
 
   String myDashboardTitle();
   String unknownDashboardTitle();
@@ -37,6 +40,7 @@
   String allMergedChanges();
 
   String changeTableColumnSubject();
+  String changeTableColumnStatus();
   String changeTableColumnOwner();
   String changeTableColumnReviewers();
   String changeTableColumnProject();
@@ -52,7 +56,12 @@
   String expandCollapseDependencies();
   String previousPatchSet();
   String nextPatchSet();
+  String keyReloadChange();
+  String keyReloadSearch();
   String keyPublishComments();
+  String keyEditTopic();
+  String keyEditMessage();
+  String keyAddReviewers();
 
   String patchTableColumnName();
   String patchTableColumnComments();
@@ -62,6 +71,7 @@
   String patchTableDiffUnified();
   String patchTableDownloadPreImage();
   String patchTableDownloadPostImage();
+  String patchTableBinary();
   String commitMessage();
   String fileCommentHeader();
 
@@ -119,6 +129,9 @@
   String messageCollapseAll();
   String messageNeedsRebaseOrHasDependency();
 
+  String sideBySide();
+  String unifiedDiff();
+
   String patchSetInfoAuthor();
   String patchSetInfoCommitter();
   String patchSetInfoDownload();
@@ -137,6 +150,12 @@
   String editCommitMessageToolTip();
   String titleEditCommitMessage();
 
+  String buttonCherryPickChangeBegin();
+  String buttonCherryPickChangeSend();
+  String headingCherryPickBranch();
+  String cherryPickCommitMessage();
+  String cherryPickTitle();
+
   String buttonAbandonChangeBegin();
   String buttonAbandonChangeSend();
   String headingAbandonMessage();
@@ -179,4 +198,12 @@
   String months();
   String year();
   String years();
+
+  String oneSecondAgo();
+  String oneMinuteAgo();
+  String oneHourAgo();
+  String oneDayAgo();
+  String oneWeekAgo();
+  String oneMonthAgo();
+  String oneYearAgo();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 4c12378..60fc214 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -3,6 +3,9 @@
 statusLongMerged = Merged
 statusLongAbandoned = Abandoned
 statusLongDraft = Draft
+readyToSubmit = Ready to Submit
+mergeConflict = Merge Conflict
+notCurrent = Not Current
 
 starredHeading = Starred Changes
 watchedHeading = Open Changes of Watched Projects
@@ -17,6 +20,7 @@
 allMergedChanges = All merged changes
 
 changeTableColumnSubject = Subject
+changeTableColumnStatus = Status
 changeTableColumnOwner = Owner
 changeTableColumnReviewers = Reviewers
 changeTableColumnProject = Project
@@ -32,7 +36,13 @@
 expandCollapseDependencies = Expands / Collapses dependencies section
 previousPatchSet = Previous patch set
 nextPatchSet = Next patch set
+keyReloadChange = Reload change
+keyReloadSearch = Reload change list
 keyPublishComments = Review and publish comments
+keyEditTopic = Edit change topic
+keyEditMessage = Edit commit message
+keyAddReviewers = Add reviewers
+
 
 patchTableColumnName = File Path
 patchTableColumnComments = Comments
@@ -42,6 +52,7 @@
 patchTableDiffUnified = Unified
 patchTableDownloadPreImage = old
 patchTableDownloadPostImage = new
+patchTableBinary = Binary
 commitMessage = Commit Message
 fileCommentHeader = File Comment:
 
@@ -96,6 +107,9 @@
 messageCollapseAll = Collapse All
 messageNeedsRebaseOrHasDependency = Need Rebase or Has Dependency
 
+sideBySide = Side by Side
+unifiedDiff = Unified Diff
+
 patchSetInfoAuthor = Author
 patchSetInfoCommitter = Committer
 patchSetInfoDownload = Download
@@ -122,6 +136,12 @@
 editCommitMessageToolTip = Edit Commit Message
 titleEditCommitMessage = Create New Patch Set
 
+buttonCherryPickChangeBegin = Cherry Pick To
+buttonCherryPickChangeSend = Cherry Pick Change
+headingCherryPickBranch = Cherry Pick to Branch:
+cherryPickCommitMessage = Cherry Pick Commit Message:
+cherryPickTitle = Code Review - Cherry Pick Change to Another Branch
+
 buttonRestoreChangeBegin = Restore Change
 restoreChangeTitle = Code Review - Restore Change
 headingRestoreMessage = Restore Message:
@@ -160,3 +180,11 @@
 months = months
 years = years
 year = year
+
+oneSecondAgo = 1 second ago
+oneMinuteAgo = 1 minute ago
+oneHourAgo = 1 hour ago
+oneDayAgo = 1 day ago
+oneWeekAgo = 1 week ago
+oneMonthAgo = 1 month ago
+oneYearAgo = 1 year ago
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
index 2fcb7e8..6b13ba0a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
@@ -37,12 +37,12 @@
     initWidget(hp);
   }
 
-  public void display(ChangeDetail chg, Boolean starred, Boolean canEditCommitMessage,
+  public void display(ChangeDetail changeDetail, Boolean starred, Boolean canEditCommitMessage,
       PatchSetInfo info, AccountInfoCache acc,
       SubmitTypeRecord submitTypeRecord,
       CommentLinkProcessor commentLinkProcessor) {
-    infoBlock.display(chg, acc, submitTypeRecord);
-    messageBlock.display(chg.getChange().currentPatchSetId(), starred,
+    infoBlock.display(changeDetail, acc, submitTypeRecord);
+    messageBlock.display(changeDetail.getChange().currentPatchSetId(), starred,
         canEditCommitMessage, info.getMessage(), commentLinkProcessor);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index 00693d9..f82abee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.diff.FileInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -27,12 +30,16 @@
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 
 import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 public class ChangeInfo extends JavaScriptObject {
   public final void init() {
-    if (labels0() != null) {
-      labels0().copyKeysIntoChildren("_name");
+    if (all_labels() != null) {
+      all_labels().copyKeysIntoChildren("_name");
     }
   }
 
@@ -69,7 +76,7 @@
   }
 
   public final Set<String> labels() {
-    return labels0().keySet();
+    return all_labels().keySet();
   }
 
   public final native String id() /*-{ return this.id; }-*/;
@@ -86,22 +93,26 @@
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
   public final native String _sortkey() /*-{ return this._sortkey; }-*/;
-  private final native NativeMap<LabelInfo> labels0() /*-{ return this.labels; }-*/;
+  public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
+  public final native String current_revision() /*-{ return this.current_revision; }-*/;
+  public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
+  public final native RevisionInfo revision(String n) /*-{ return this.revisions[n]; }-*/;
+  public final native JsArray<MessageInfo> messages() /*-{ return this.messages; }-*/;
 
   public final native boolean has_permitted_labels()
   /*-{ return this.hasOwnProperty('permitted_labels') }-*/;
-  private final native NativeMap<JavaScriptObject> _permitted_labels()
+  public final native NativeMap<JsArrayString> permitted_labels()
   /*-{ return this.permitted_labels; }-*/;
-  public final Set<String> permitted_labels() {
-    return Natives.keys(_permitted_labels());
-  }
   public final native JsArrayString permitted_values(String n)
   /*-{ return this.permitted_labels[n]; }-*/;
 
   public final native JsArray<AccountInfo> removable_reviewers()
   /*-{ return this.removable_reviewers; }-*/;
 
+  public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+  public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
+
   final native int _number() /*-{ return this._number; }-*/;
   final native boolean _more_changes()
   /*-{ return this._more_changes ? true : false; }-*/;
@@ -130,9 +141,17 @@
     public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
 
     public final native JsArray<ApprovalInfo> all() /*-{ return this.all; }-*/;
+    public final ApprovalInfo for_user(int user) {
+      JsArray<ApprovalInfo> all = all();
+      for (int i = 0; all != null && i < all.length(); i++) {
+        if (all.get(i)._account_id() == user) {
+          return all.get(i);
+        }
+      }
+      return null;
+    }
 
     private final native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
-
     public final Set<String> values() {
       return Natives.keys(_values());
     }
@@ -147,15 +166,130 @@
       return 0;
     }-*/;
 
+    public final String max_value() {
+      return LabelValue.formatValue(value_set().last());
+    }
+
+    public final SortedSet<Short> value_set() {
+      SortedSet<Short> values = new TreeSet<Short>();
+      for (String v : values()) {
+        values.add(parseValue(v));
+      }
+      return values;
+    }
+
+    public static final short parseValue(String formatted) {
+      if (formatted.startsWith("+")) {
+        formatted = formatted.substring(1);
+      } else if (formatted.startsWith(" ")) {
+        formatted = formatted.trim();
+      }
+      return Short.parseShort(formatted);
+    }
+
     protected LabelInfo() {
     }
   }
 
   public static class ApprovalInfo extends AccountInfo {
     public final native boolean has_value() /*-{ return this.hasOwnProperty('value'); }-*/;
-    public final native short value() /*-{ return this.value; }-*/;
+    public final native short value() /*-{ return this.value || 0; }-*/;
 
     protected ApprovalInfo() {
     }
   }
+
+  public static class RevisionInfo extends JavaScriptObject {
+    public final native int _number() /*-{ return this._number; }-*/;
+    public final native String name() /*-{ return this.name; }-*/;
+    public final native boolean draft() /*-{ return this.draft || false; }-*/;
+    public final native boolean has_draft_comments() /*-{ return this.has_draft_comments || false; }-*/;
+    public final native CommitInfo commit() /*-{ return this.commit; }-*/;
+    public final native void set_commit(CommitInfo c) /*-{ this.commit = c; }-*/;
+
+    public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
+    public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
+
+    public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+    public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
+
+    public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+    public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
+
+    public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
+      Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
+        @Override
+        public int compare(RevisionInfo a, RevisionInfo b) {
+          return a._number() - b._number();
+        }
+      });
+    }
+
+    protected RevisionInfo () {
+    }
+  }
+
+  public static class FetchInfo extends JavaScriptObject {
+    public final native String url() /*-{ return this.url }-*/;
+    public final native String ref() /*-{ return this.ref }-*/;
+    public final native NativeMap<NativeString> commands() /*-{ return this.commands }-*/;
+    public final native String command(String n) /*-{ return this.commands[n]; }-*/;
+
+    protected FetchInfo () {
+    }
+  }
+
+  public static class CommitInfo extends JavaScriptObject {
+    public final native String commit() /*-{ return this.commit; }-*/;
+    public final native JsArray<CommitInfo> parents() /*-{ return this.parents; }-*/;
+    public final native GitPerson author() /*-{ return this.author; }-*/;
+    public final native GitPerson committer() /*-{ return this.committer; }-*/;
+    public final native String subject() /*-{ return this.subject; }-*/;
+    public final native String message() /*-{ return this.message; }-*/;
+
+    protected CommitInfo() {
+    }
+  }
+
+  public static class GitPerson extends JavaScriptObject {
+    public final native String name() /*-{ return this.name; }-*/;
+    public final native String email() /*-{ return this.email; }-*/;
+    private final native String dateRaw() /*-{ return this.date; }-*/;
+
+    public final Timestamp date() {
+      return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
+    }
+
+    protected GitPerson() {
+    }
+  }
+
+  public static class MessageInfo extends JavaScriptObject {
+    public final native AccountInfo author() /*-{ return this.author; }-*/;
+    public final native String message() /*-{ return this.message; }-*/;
+    private final native String dateRaw() /*-{ return this.date; }-*/;
+
+    public final Timestamp date() {
+      return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
+    }
+
+    protected MessageInfo() {
+    }
+  }
+
+  public static class MergeableInfo extends JavaScriptObject {
+    public final native String submit_type() /*-{ return this.submit_type }-*/;
+    public final native boolean mergeable() /*-{ return this.mergeable }-*/;
+
+    protected MergeableInfo() {
+    }
+  }
+
+  public static class IncludedInInfo extends JavaScriptObject {
+    public final native JsArrayString branches() /*-{ return this.branches; }-*/;
+    public final native JsArrayString tags() /*-{ return this.tags; }-*/;
+
+    protected IncludedInInfo() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index 7c41ea1..773313a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -28,13 +28,18 @@
 
   /** Run 2 or more queries in a single remote invocation. */
   public static void query(
-      AsyncCallback<JsArray<ChangeList>> callback, String... queries) {
+      AsyncCallback<JsArray<ChangeList>> callback,
+      EnumSet<ListChangesOption> options,
+      String... queries) {
     assert queries.length >= 2; // At least 2 is required for correct result.
     RestApi call = new RestApi(URI);
     for (String q : queries) {
       call.addParameterRaw("q", KeyUtil.encode(q));
     }
-    addOptions(call, ListChangesOption.LABELS);
+
+    EnumSet<ListChangesOption> o = EnumSet.of(ListChangesOption.LABELS);
+    o.addAll(options);
+    addOptions(call, o);
     call.get(callback);
   }
 
@@ -45,7 +50,7 @@
     if (limit > 0) {
       call.addParameter("n", limit);
     }
-    addOptions(call, ListChangesOption.LABELS);
+    addOptions(call, EnumSet.of(ListChangesOption.LABELS));
     if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
       call.addParameter("P", sortkey);
     }
@@ -59,16 +64,14 @@
     if (limit > 0) {
       call.addParameter("n", limit);
     }
-    addOptions(call, ListChangesOption.LABELS);
+    addOptions(call, EnumSet.of(ListChangesOption.LABELS));
     if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
       call.addParameter("N", sortkey);
     }
     call.get(callback);
   }
 
-  private static void addOptions(
-      RestApi call, ListChangesOption option1, ListChangesOption... options) {
-    EnumSet<ListChangesOption> s = EnumSet.of(option1, options);
+  public static void addOptions(RestApi call, EnumSet<ListChangesOption> s) {
     call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 098fe07..4d1ec7e 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
@@ -24,6 +24,7 @@
 
   String revertChangeDefaultMessage(String commitMsg, String commitId);
 
+  String cherryPickedChangeDefaultMessage(String commitMsg, String commitId);
   String changeScreenTitleId(String changeId);
   String outdatedHeader(int outdated);
   String patchSetHeader(int id);
@@ -33,6 +34,7 @@
   String patchTableComments(@PluralCount int count);
   String patchTableDrafts(@PluralCount int count);
   String patchTableSize_Modify(int insertions, int deletions);
+  String patchTableSize_LongModify(int insertions, int deletions);
   String patchTableSize_Lines(@PluralCount int insertions);
 
   String removeReviewer(String fullName);
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 e02c27c..f7dee85 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
@@ -6,6 +6,7 @@
 changesAbandonedInProject = Abandoned Changes In {0}
 
 revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
+cherryPickedChangeDefaultMessage = {0}\n(cherry picked from commit {1})
 
 changeScreenTitleId = Change {0}
 outdatedHeader = Change depends on {0} outdated change(s) and should be rebased on the latest patch sets.
@@ -16,6 +17,7 @@
 patchTableComments = {0} comments
 patchTableDrafts = {0} drafts
 patchTableSize_Modify = +{0}, -{1}
+patchTableSize_LongModify = {0} inserted, {1} deleted
 patchTableSize_Lines = {0} lines
 
 removeReviewer = Remove reviewer {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index c69f915..f2b74d6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -288,7 +288,7 @@
               // Handled by last callback's onFailure.
             }
           }));
-      ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.add(
+      ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.addFinal(
           new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
             @Override
             public void onSuccess(
@@ -339,13 +339,11 @@
       patchesList.addItem(Util.C.baseDiffItem());
     }
     for (PatchSet pId : detail.getPatchSets()) {
-      if (patchesList != null) {
-        patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId
-            .getId().toString());
-      }
+      patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId
+          .getId().toString());
     }
 
-    if (diffBaseId != null && patchesList != null) {
+    if (diffBaseId != null) {
       patchesList.setSelectedIndex(diffBaseId.get());
     }
 
@@ -422,7 +420,7 @@
     final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE);
 
     CommentVisibilityStrategy commentVisibilityStrategy =
-        CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+        CommentVisibilityStrategy.EXPAND_RECENT;
     if (Gerrit.isSignedIn()) {
       commentVisibilityStrategy = Gerrit.getUserAccount()
           .getGeneralPreferences().getCommentVisibilityStrategy();
@@ -457,16 +455,16 @@
       switch (commentVisibilityStrategy) {
         case COLLAPSE_ALL:
           break;
-        case EXPAND_RECENT:
-          isOpen = isRecent;
-          break;
         case EXPAND_ALL:
           isOpen = true;
           break;
         case EXPAND_MOST_RECENT:
-        default:
           isOpen = i == msgList.size() - 1;
           break;
+        case EXPAND_RECENT:
+        default:
+          isOpen = isRecent;
+          break;
       }
       cp.setOpen(isOpen);
       comments.add(cp);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index 4694272..cedbd4b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -48,11 +48,12 @@
 public class ChangeTable2 extends NavigationTable<ChangeInfo> {
   private static final int C_STAR = 1;
   private static final int C_SUBJECT = 2;
-  private static final int C_OWNER = 3;
-  private static final int C_PROJECT = 4;
-  private static final int C_BRANCH = 5;
-  private static final int C_LAST_UPDATE = 6;
-  private static final int BASE_COLUMNS = 7;
+  private static final int C_STATUS = 3;
+  private static final int C_OWNER = 4;
+  private static final int C_PROJECT = 5;
+  private static final int C_BRANCH = 6;
+  private static final int C_LAST_UPDATE = 7;
+  private static final int BASE_COLUMNS = 8;
 
   private final List<Section> sections;
   private int columns;
@@ -70,6 +71,7 @@
     sections = new ArrayList<Section>();
     table.setText(0, C_STAR, "");
     table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
+    table.setText(0, C_STATUS, Util.C.changeTableColumnStatus());
     table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
     table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
     table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
@@ -90,6 +92,8 @@
         }
         if (cell.getCellIndex() == C_STAR) {
           // Don't do anything (handled by star itself).
+        } else if (cell.getCellIndex() == C_STATUS) {
+          // Don't do anything.
         } else if (cell.getCellIndex() == C_OWNER) {
           // Don't do anything.
         } else if (getRowItem(cell.getRowIndex()) != null) {
@@ -108,7 +112,7 @@
   protected void onOpenRow(final int row) {
     final ChangeInfo c = getRowItem(row);
     final Change.Id id = c.legacy_id();
-    Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+    Gerrit.display(PageLinks.toChange(id));
   }
 
   private void insertNoneRow(final int row) {
@@ -191,11 +195,12 @@
     }
 
     String subject = Util.cropSubject(c.subject());
+    table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
+
     Change.Status status = c.status();
     if (status != Change.Status.NEW) {
-      subject += " (" + Util.toLongString(status) + ")";
+      table.setText(row, C_STATUS, Util.toLongString(status));
     }
-    table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
 
     if (c.owner() != null) {
       table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
@@ -225,7 +230,8 @@
 
       LabelInfo label = c.label(name);
       if (label == null) {
-        table.clearCell(row, col);
+        fmt.getElement(row, col).setTitle(Gerrit.C.labelNotApplicable());
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().labelNotApplicable());
         continue;
       }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
new file mode 100644
index 0000000..d87cb5e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class CommentApi {
+
+  public static void comments(PatchSet.Id id,
+      AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+    revision(id, "comments").get(cb);
+  }
+
+  public static void comment(PatchSet.Id id, String commentId,
+      AsyncCallback<CommentInfo> cb) {
+    revision(id, "comments").id(commentId).get(cb);
+  }
+
+  public static void drafts(PatchSet.Id id,
+      AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+    revision(id, "drafts").get(cb);
+  }
+
+  public static void draft(PatchSet.Id id, String draftId,
+      AsyncCallback<CommentInfo> cb) {
+    revision(id, "drafts").id(draftId).get(cb);
+  }
+
+  public static void createDraft(PatchSet.Id id, CommentInput content,
+      AsyncCallback<CommentInfo> cb) {
+    revision(id, "drafts").put(content, cb);
+  }
+
+  public static void updateDraft(PatchSet.Id id, String draftId,
+      CommentInput content, AsyncCallback<CommentInfo> cb) {
+    revision(id, "drafts").id(draftId).put(content, cb);
+  }
+
+  public static void deleteDraft(PatchSet.Id id, String draftId,
+      AsyncCallback<JavaScriptObject> cb) {
+    revision(id, "drafts").id(draftId).delete(cb);
+  }
+
+  private static RestApi revision(PatchSet.Id id, String type) {
+    return ChangeApi.revision(id).view(type);
+  }
+
+  private CommentApi() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
new file mode 100644
index 0000000..34c0638
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.diff.CommentRange;
+import com.google.gerrit.common.changes.Side;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+
+public class CommentInfo extends JavaScriptObject {
+  public static CommentInfo createRange(String path, Side side, int line,
+      String in_reply_to, String message, CommentRange range) {
+    CommentInfo info = createFile(path, side, in_reply_to, message);
+    info.setRange(range);
+    info.setLine(range == null ? line : range.end_line());
+    return info;
+  }
+
+  public static CommentInfo createFile(String path, Side side,
+      String in_reply_to, String message) {
+    CommentInfo info = createObject().cast();
+    info.setPath(path);
+    info.setSide(side);
+    info.setInReplyTo(in_reply_to);
+    info.setMessage(message);
+    return info;
+  }
+
+  private final native void setId(String id) /*-{ this.id = id; }-*/;
+  private final native void setPath(String path) /*-{ this.path = path; }-*/;
+
+  private final void setSide(Side side) {
+    setSideRaw(side.toString());
+  }
+  private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
+
+  private final native void setLine(int line) /*-{ this.line = line; }-*/;
+
+  private final native void setInReplyTo(String in_reply_to) /*-{
+    this.in_reply_to = in_reply_to;
+  }-*/;
+
+  private final native void setMessage(String message) /*-{ this.message = message; }-*/;
+
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String path() /*-{ return this.path; }-*/;
+
+  public final Side side() {
+    String s = sideRaw();
+    return s != null
+        ? Side.valueOf(s)
+        : Side.REVISION;
+  }
+  private final native String sideRaw() /*-{ return this.side }-*/;
+
+  public final native int line() /*-{ return this.line; }-*/;
+  public final native String in_reply_to() /*-{ return this.in_reply_to; }-*/;
+  public final native String message() /*-{ return this.message; }-*/;
+
+  public final Timestamp updated() {
+    String updatedRaw = updatedRaw();
+    return updatedRaw == null
+        ? null
+        : JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+  }
+  private final native String updatedRaw() /*-{ return this.updated; }-*/;
+
+  public final native AccountInfo author() /*-{ return this.author; }-*/;
+
+  public final native boolean has_line() /*-{ return this.hasOwnProperty('line'); }-*/;
+
+  public final native CommentRange range() /*-{ return this.range; }-*/;
+
+  public final native void setRange(CommentRange range) /*-{ this.range = range; }-*/;
+
+  protected CommentInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
new file mode 100644
index 0000000..c96a67f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.diff.CommentRange;
+import com.google.gerrit.common.changes.Side;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+
+public class CommentInput extends JavaScriptObject {
+  public static CommentInput create(CommentInfo original) {
+    CommentInput input = createObject().cast();
+    input.setId(original.id());
+    input.setPath(original.path());
+    input.setSide(original.side());
+    if (original.has_line()) {
+      input.setLine(original.line());
+    }
+    input.setRange(original.range());
+    input.setInReplyTo(original.in_reply_to());
+    input.setMessage(original.message());
+    return input;
+  }
+
+  public final native void setId(String id) /*-{ this.id = id; }-*/;
+  public final native void setPath(String path) /*-{ this.path = path; }-*/;
+
+  public final void setSide(Side side) {
+    setSideRaw(side.toString());
+  }
+  private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
+
+  public final native void setLine(int line) /*-{ this.line = line; }-*/;
+
+  public final native void setInReplyTo(String in_reply_to) /*-{
+    this.in_reply_to = in_reply_to;
+  }-*/;
+
+  public final native void setMessage(String message) /*-{ this.message = message; }-*/;
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String path() /*-{ return this.path; }-*/;
+
+  public final Side side() {
+    String s = sideRaw();
+    return s != null
+        ? Side.valueOf(s)
+        : Side.REVISION;
+  }
+  private final native String sideRaw() /*-{ return this.side }-*/;
+
+  public final native int line() /*-{ return this.line; }-*/;
+  public final native String in_reply_to() /*-{ return this.in_reply_to; }-*/;
+  public final native String message() /*-{ return this.message; }-*/;
+
+  public final Timestamp updated() {
+    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+  }
+  private final native String updatedRaw() /*-{ return this.updated; }-*/;
+
+  public final native boolean has_line() /*-{ return this.hasOwnProperty('line'); }-*/;
+
+  public final native CommentRange range() /*-{ return this.range; }-*/;
+
+  public final native void setRange(CommentRange range) /*-{ this.range = range; }-*/;
+
+  protected CommentInput() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
index 198480e..f612dcd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
@@ -46,7 +46,7 @@
   interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {
   }
 
-  private static Binder uiBinder = GWT.create(Binder.class);
+  private static final Binder uiBinder = GWT.create(Binder.class);
 
   private KeyCommandSet keysAction;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
index 7b387ab..3002e48 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -19,10 +19,12 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.changes.ListChangesOption;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.http.client.URL;
 
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.ListIterator;
 
@@ -103,6 +105,7 @@
               finishDisplay();
             }
           },
+          EnumSet.noneOf(ListChangesOption.class),
           queries.toArray(new String[queries.size()]));
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index c21c68d..b2ccbcb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -71,6 +71,18 @@
             .changeTablePagePrev(), prev));
         keysNavigation.add(new DoLinkCommand(0, 'n', Util.C
             .changeTablePageNext(), next));
+
+        keysNavigation.add(new DoLinkCommand(0, '[', Util.C
+            .changeTablePagePrev(), prev));
+        keysNavigation.add(new DoLinkCommand(0, ']', Util.C
+            .changeTablePageNext(), next));
+
+        keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
+          @Override
+          public void onKeyPress(final KeyPressEvent event) {
+            Gerrit.display(getToken());
+          }
+        });
       }
     };
     section = new ChangeTable2.Section();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index ca326cd..f1bb5b2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -13,21 +13,25 @@
 // limitations under the License.
 
 package com.google.gerrit.client.changes;
-
 import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.download.DownloadPanel;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.CommentedActionDialog;
+import com.google.gerrit.client.ui.ActionDialog;
+import com.google.gerrit.client.ui.CherryPickDialog;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.common.data.UiCommandDetail;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
@@ -35,18 +39,20 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.OpenEvent;
 import com.google.gwt.event.logical.shared.OpenHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusWidget;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -174,6 +180,7 @@
           if (changeDetail.isCurrentPatchSet(detail)) {
             populateActions(detail);
           }
+          populateCommands(detail);
         }
         if (detail.getPatchSet().isDraft()) {
           if (changeDetail.canPublish()) {
@@ -383,6 +390,52 @@
       actionsPanel.add(b);
     }
 
+    if (changeDetail.canCherryPick()) {
+      final Button b = new Button(Util.C.buttonCherryPickChangeBegin());
+      b.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          b.setEnabled(false);
+          new CherryPickDialog(b, changeDetail.getChange().getProject()) {
+            {
+              sendButton.setText(Util.C.buttonCherryPickChangeSend());
+              if (changeDetail.getChange().getStatus().isClosed()) {
+                message.setText(Util.M.cherryPickedChangeDefaultMessage(
+                    detail.getInfo().getMessage().trim(),
+                    detail.getPatchSet().getRevision().get()));
+              } else {
+                message.setText(detail.getInfo().getMessage().trim());
+              }
+            }
+
+            @Override
+            public void onSend() {
+              ChangeApi.cherrypick(changeDetail.getChange().getChangeId(),
+                  patchSet.getRevision().get(),
+                  getDestinationBranch(),
+                  getMessageText(),
+                  new GerritCallback<ChangeInfo>() {
+                    @Override
+                    public void onSuccess(ChangeInfo result) {
+                      sent = true;
+                      Gerrit.display(PageLinks.toChange(new Change.Id(result
+                          ._number())));
+                      hide();
+                    }
+
+                    @Override
+                    public void onFailure(Throwable caught) {
+                      enableButtons(true);
+                      super.onFailure(caught);
+                    }
+                  });
+            }
+          }.center();
+        }
+      });
+      actionsPanel.add(b);
+    }
+
     if (changeDetail.canAbandon()) {
       final Button b = new Button(Util.C.buttonAbandonChangeBegin());
       b.addClickHandler(new ClickHandler() {
@@ -498,6 +551,47 @@
     }
   }
 
+  private void populateCommands(final PatchSetDetail detail) {
+    for (final UiCommandDetail cmd : detail.getCommands()) {
+      final Button b = new Button();
+      b.setText(cmd.label);
+      b.setEnabled(cmd.enabled);
+      b.setTitle(cmd.title);
+      b.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          b.setEnabled(false);
+          AsyncCallback<NativeString> cb =
+              new AsyncCallback<NativeString>() {
+                @Override
+                public void onFailure(Throwable caught) {
+                  b.setEnabled(true);
+                  new ErrorDialog(caught).center();
+                }
+
+                @Override
+                public void onSuccess(NativeString msg) {
+                  b.setEnabled(true);
+                  if (msg != null && !msg.asString().isEmpty()) {
+                    Window.alert(msg.asString());
+                  }
+                  Gerrit.display(PageLinks.toChange(patchSet.getId()));
+                }
+              };
+          RestApi api = ChangeApi.revision(patchSet.getId()).view(cmd.id);
+          if ("PUT".equalsIgnoreCase(cmd.method)) {
+            api.put(JavaScriptObject.createObject(), cb);
+          } else if ("DELETE".equalsIgnoreCase(cmd.method)) {
+            api.delete(cb);
+          } else {
+            api.post(JavaScriptObject.createObject(), cb);
+          }
+        }
+      });
+      actionsPanel.add(b);
+    }
+  }
+
   private void populateReviewAction() {
     final Button b = new Button(Util.C.buttonReview());
     b.addClickHandler(new ClickHandler() {
@@ -633,25 +727,4 @@
       patchTable.setActive(active);
     }
   }
-
-  private abstract class ActionDialog extends CommentedActionDialog<ChangeDetail> {
-    public ActionDialog(final FocusWidget enableOnFailure, final boolean redirect,
-        String dialogTitle, String dialogHeading) {
-      super(dialogTitle, dialogHeading, new ChangeDetailCache.IgnoreErrorCallback() {
-          @Override
-          public void onSuccess(ChangeDetail result) {
-            if (redirect) {
-              Gerrit.display(PageLinks.toChange(result.getChange().getId()));
-            } else {
-              super.onSuccess(result);
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            enableOnFailure.setEnabled(true);
-          }
-        });
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
index d6c1de1..42705db 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
@@ -55,10 +55,8 @@
 public class PatchTable extends Composite {
   public interface PatchValidator {
     /**
-     * Returns true if patch is valid
-     *
      * @param patch
-     * @return
+     * @return true if patch is valid.
      */
     boolean isValid(Patch patch);
   }
@@ -836,7 +834,7 @@
    * @param validators
    * @param loopAround loops back around to the front and traverses if this is
    *        true
-   * @return
+   * @return index of next valid patch, or -1 if no valid patches
    */
   public int getNextPatch(int currentIndex, boolean loopAround,
       PatchValidator... validators) {
@@ -851,7 +849,7 @@
    * @param validators
    * @param loopAround
    * @param maxIndex will only traverse up to this index
-   * @return
+   * @return index of next valid patch, or -1 if no valid patches
    */
   private int getNextPatchHelper(int currentIndex, boolean loopAround,
       int maxIndex, PatchValidator... validators) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 4e710db..6e9b6d6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -164,7 +163,7 @@
             // Handled by ScreenLoadCallback.onFailure().
           }
         }));
-    Util.DETAIL_SVC.patchSetPublishDetail(patchSetId, cbs.addGwtjsonrpc(
+    Util.DETAIL_SVC.patchSetPublishDetail(patchSetId, cbs.addFinal(
         new ScreenLoadCallback<PatchSetPublishDetail>(this) {
           @Override
           protected void preDisplay(final PatchSetPublishDetail result) {
@@ -307,7 +306,7 @@
 
       if (lastState != null && patchSetId.equals(lastState.patchSetId)
           && lastState.approvals.containsKey(label.name())) {
-        b.setValue(lastState.approvals.get(label.name()) == value);
+        b.setValue(lastState.approvals.get(label.name()).equals(value));
       } else {
         b.setValue(b.parseValue() == (prior != null ? prior : 0));
       }
@@ -406,7 +405,6 @@
   private void onSend2(final boolean submit) {
     ReviewInput data = ReviewInput.create();
     data.message(ChangeApi.emptyToNull(message.getText().trim()));
-    data.init();
     for (final ValueRadioButton b : approvalButtons) {
       if (b.getValue()) {
         data.label(b.label.name(), b.parseValue());
@@ -436,23 +434,6 @@
         });
   }
 
-  private static class ReviewInput extends JavaScriptObject {
-    static ReviewInput create() {
-      return (ReviewInput) createObject();
-    }
-
-    final native void message(String m) /*-{ if(m)this.message=m; }-*/;
-    final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
-    final native void init() /*-{
-      this.labels = {};
-      this.strict_labels = true;
-      this.drafts = 'PUBLISH';
-    }-*/;
-
-    protected ReviewInput() {
-    }
-  }
-
   private void submit() {
     ChangeApi.submit(patchSetId.getParentKey().get(), revision,
       new GerritCallback<SubmitInfo>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 01e294f..12bcf63 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -55,7 +55,7 @@
           if (result.length() == 1 && isSingleQuery(query)) {
             ChangeInfo c = result.get(0);
             Change.Id id = c.legacy_id();
-            Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+            Gerrit.display(PageLinks.toChange(id));
           } else {
             display(result);
             QueryScreen.this.display();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java
new file mode 100644
index 0000000..3508c3d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ReviewInfo extends JavaScriptObject {
+
+  public final native NativeMap<?> labels() /*-{ return this.labels }-*/;
+
+  protected ReviewInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
new file mode 100644
index 0000000..fa3d784
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ReviewInput extends JavaScriptObject {
+  public static enum NotifyHandling {
+    NONE, OWNER, OWNER_REVIEWERS, ALL;
+  }
+
+  public static ReviewInput create() {
+    ReviewInput r = createObject().cast();
+    r.init();
+    return r;
+  }
+
+  public final native void message(String m) /*-{ if(m)this.message=m; }-*/;
+  public final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
+
+  public final void notify(NotifyHandling e) {
+    _notify(e.name());
+  }
+  private final native void _notify(String n) /*-{ this.notify=n; }-*/;
+
+  private final native void init() /*-{
+    this.labels = {};
+    this.strict_labels = true;
+    this.drafts = 'PUBLISH';
+  }-*/;
+
+  protected ReviewInput() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
new file mode 100644
index 0000000..53d618c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Cache of PatchSet.Id to revision SHA-1 strings. */
+public class RevisionInfoCache {
+  private static final int LIMIT = 10;
+  private static final RevisionInfoCache IMPL = new RevisionInfoCache();
+
+  public static void add(Change.Id change, RevisionInfo info) {
+    IMPL.psToCommit.put(
+        new PatchSet.Id(change, info._number()),
+        info.name());
+  }
+
+  static String get(PatchSet.Id id) {
+    return IMPL.psToCommit.get(id);
+  }
+
+  private final LinkedHashMap<PatchSet.Id, String> psToCommit;
+
+  @SuppressWarnings("serial")
+  private RevisionInfoCache() {
+    psToCommit = new LinkedHashMap<PatchSet.Id, String>(LIMIT) {
+      @Override
+      protected boolean removeEldestEntry(Map.Entry<PatchSet.Id, String> e) {
+        return size() > LIMIT;
+      }
+    };
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
index 7d2084a..b097bd8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -15,19 +15,23 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.data.ToggleStarRequest;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.web.bindery.event.shared.Event;
 import com.google.web.bindery.event.shared.HandlerRegistration;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 /** Supports the star icon displayed on changes and tracking the status. */
 public class StarredChanges {
   private static final Event.Type<ChangeStarHandler> TYPE =
@@ -105,57 +109,52 @@
   public static void toggleStar(
       final Change.Id changeId,
       final boolean newValue) {
-    if (next == null) {
-      next = new ToggleStarRequest();
-    }
-    next.toggle(changeId, newValue);
+    pending.put(changeId, newValue);
     fireChangeStarEvent(changeId, newValue);
     if (!busy) {
-      start();
+      startRequest();
     }
   }
 
-  private static ToggleStarRequest next;
   private static boolean busy;
+  private static final Map<Change.Id, Boolean> pending =
+      new LinkedHashMap<Change.Id, Boolean>(4);
 
-  private static void start() {
-    final ToggleStarRequest req = next;
-    next = null;
+  private static void startRequest() {
     busy = true;
 
-    Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
+    final Change.Id id = pending.keySet().iterator().next();
+    final boolean starred = pending.remove(id);
+    RestApi call = AccountApi.self().view("starred.changes").id(id.get());
+    AsyncCallback<JavaScriptObject> cb = new AsyncCallback<JavaScriptObject>() {
       @Override
-      public void onSuccess(VoidResult result) {
-        if (next != null) {
-          start();
-        } else {
+      public void onSuccess(JavaScriptObject none) {
+        if (pending.isEmpty()) {
           busy = false;
+        } else {
+          startRequest();
         }
       }
 
       @Override
       public void onFailure(Throwable caught) {
-        rollback(req);
-        if (next != null) {
-          rollback(next);
-          next = null;
+        if (!starred && RestApi.isStatus(caught, 404)) {
+          onSuccess(null);
+          return;
         }
-        busy = false;
-        super.onFailure(caught);
-      }
-    });
-  }
 
-  private static void rollback(ToggleStarRequest req) {
-    if (req.getAddSet() != null) {
-      for (Change.Id id : req.getAddSet()) {
-        fireChangeStarEvent(id, false);
+        fireChangeStarEvent(id, !starred);
+        for (Map.Entry<Change.Id, Boolean> e : pending.entrySet()) {
+          fireChangeStarEvent(e.getKey(), !e.getValue());
+        }
+        pending.clear();
+        busy = false;
       }
-    }
-    if (req.getRemoveSet() != null) {
-      for (Change.Id id : req.getRemoveSet()) {
-        fireChangeStarEvent(id, true);
-      }
+    };
+    if (starred) {
+      call.put(cb);
+    } else {
+      call.delete(cb);
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
index 70bf4b6..bd97790 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
@@ -18,13 +18,13 @@
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
-class SubmitFailureDialog extends ErrorDialog {
-  static boolean isConflict(Throwable err) {
+public class SubmitFailureDialog extends ErrorDialog {
+  public static boolean isConflict(Throwable err) {
     return err instanceof RemoteJsonException
         && 409 == ((RemoteJsonException) err).getCode();
   }
 
-  SubmitFailureDialog(String msg) {
+  public SubmitFailureDialog(String msg) {
     super(new SafeHtmlBuilder().append(msg.trim()).wikify());
     setText(Util.C.submitFailed());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
index a9206b7..6d8bc30 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
 
-class SubmitInfo extends JavaScriptObject {
+public class SubmitInfo extends JavaScriptObject {
   final Change.Status status() {
     return Change.Status.valueOf(statusRaw());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
index 590ad87..76dfd58 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.common.data.ChangeDetailService;
-import com.google.gerrit.common.data.ChangeListService;
 import com.google.gerrit.common.data.ChangeManageService;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
@@ -27,7 +26,6 @@
   public static final ChangeResources R = GWT.create(ChangeResources.class);
 
   public static final ChangeDetailService DETAIL_SVC;
-  public static final ChangeListService LIST_SVC;
   public static final ChangeManageService MANAGE_SVC;
 
   private static final int SUBJECT_MAX_LENGTH = 80;
@@ -38,9 +36,6 @@
     DETAIL_SVC = GWT.create(ChangeDetailService.class);
     JsonUtil.bind(DETAIL_SVC, "rpc/ChangeDetailService");
 
-    LIST_SVC = GWT.create(ChangeListService.class);
-    JsonUtil.bind(LIST_SVC, "rpc/ChangeListService");
-
     MANAGE_SVC = GWT.create(ChangeManageService.class);
     JsonUtil.bind(MANAGE_SVC, "rpc/ChangeManageService");
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java
new file mode 100644
index 0000000..45abbd6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class CapabilityInfo extends JavaScriptObject {
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+
+  protected CapabilityInfo() {
+  }
+}
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
new file mode 100644
index 0000000..9cb6c37
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gerrit.client.extensions.TopMenuList;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * A collection of static methods which work on the Gerrit REST API for server
+ * configuration.
+ */
+public class ConfigServerApi {
+  /** map of the server wide capabilities (core & plugins). */
+  public static void capabilities(AsyncCallback<NativeMap<CapabilityInfo>> cb) {
+    new RestApi("/config/server/capabilities/").get(cb);
+  }
+
+  public static void topMenus(AsyncCallback<TopMenuList> cb) {
+    new RestApi("/config/server/top-menus").get(cb);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
new file mode 100644
index 0000000..87073af
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -0,0 +1,169 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
+import com.google.gerrit.client.diff.SidePanel.GutterWrapper;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.user.client.ui.Composite;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.TextMarker;
+import net.codemirror.lib.TextMarker.FromTo;
+
+/** An HtmlPanel for displaying a comment */
+abstract class CommentBox extends Composite {
+  static {
+    Resources.I.style().ensureInjected();
+  }
+
+  private PaddingManager widgetManager;
+  private PaddingWidgetWrapper selfWidgetWrapper;
+  private SideBySide2 parent;
+  private CodeMirror cm;
+  private DisplaySide side;
+  private DiffChunkInfo diffChunkInfo;
+  private GutterWrapper gutterWrapper;
+  private FromTo fromTo;
+  private TextMarker rangeMarker;
+  private TextMarker rangeHighlightMarker;
+
+  CommentBox(CodeMirror cm, CommentInfo info, DisplaySide side) {
+    this.cm = cm;
+    this.side = side;
+    CommentRange range = info.range();
+    if (range != null) {
+      fromTo = FromTo.create(range);
+      rangeMarker = cm.markText(
+          fromTo.getFrom(),
+          fromTo.getTo(),
+          Configuration.create()
+              .set("className", DiffTable.style.range()));
+    }
+    addDomHandler(new MouseOverHandler() {
+      @Override
+      public void onMouseOver(MouseOverEvent event) {
+        setRangeHighlight(true);
+      }
+    }, MouseOverEvent.getType());
+    addDomHandler(new MouseOutHandler() {
+      @Override
+      public void onMouseOut(MouseOutEvent event) {
+        setRangeHighlight(isOpen());
+      }
+    }, MouseOutEvent.getType());
+  }
+
+  @Override
+  protected void onLoad() {
+    resizePaddingWidget();
+  }
+
+  void resizePaddingWidget() {
+    if (!getCommentInfo().has_line()) {
+      return;
+    }
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        assert selfWidgetWrapper != null;
+        selfWidgetWrapper.getWidget().changed();
+        if (diffChunkInfo != null) {
+          parent.resizePaddingOnOtherSide(side, diffChunkInfo.getEnd());
+        } else {
+          assert widgetManager != null;
+          widgetManager.resizePaddingWidget();
+        }
+      }
+    });
+  }
+
+  abstract CommentInfo getCommentInfo();
+  abstract boolean isOpen();
+
+  void setOpen(boolean open) {
+    resizePaddingWidget();
+    setRangeHighlight(open);
+    getCm().focus();
+  }
+
+  PaddingManager getPaddingManager() {
+    return widgetManager;
+  }
+
+  void setPaddingManager(PaddingManager manager) {
+    widgetManager = manager;
+  }
+
+  void setSelfWidgetWrapper(PaddingWidgetWrapper wrapper) {
+    selfWidgetWrapper = wrapper;
+  }
+
+  PaddingWidgetWrapper getSelfWidgetWrapper() {
+    return selfWidgetWrapper;
+  }
+
+  void setDiffChunkInfo(DiffChunkInfo info) {
+    this.diffChunkInfo = info;
+  }
+
+  void setParent(SideBySide2 parent) {
+    this.parent = parent;
+  }
+
+  void setGutterWrapper(GutterWrapper wrapper) {
+    gutterWrapper = wrapper;
+  }
+
+  void setRangeHighlight(boolean highlight) {
+    if (fromTo != null) {
+      if (highlight && rangeHighlightMarker == null) {
+        rangeHighlightMarker = cm.markText(
+            fromTo.getFrom(),
+            fromTo.getTo(),
+            Configuration.create()
+                .set("className", DiffTable.style.rangeHighlight()));
+      } else if (!highlight && rangeHighlightMarker != null) {
+        rangeHighlightMarker.clear();
+        rangeHighlightMarker = null;
+      }
+    }
+  }
+
+  void clearRange() {
+    if (rangeMarker != null) {
+      rangeMarker.clear();
+    }
+  }
+
+  GutterWrapper getGutterWrapper() {
+    return gutterWrapper;
+  }
+
+  DisplaySide getSide() {
+    return side;
+  }
+
+  CodeMirror getCm() {
+    return cm;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
new file mode 100644
index 0000000..36f57b9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
@@ -0,0 +1,81 @@
+/* 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.
+ */
+
+.commentBox {
+  position: relative;
+  width: 679px;
+  min-height: 16px;
+  font-family: sans-serif;
+  background-color: #fcfa96;
+  border: 1px solid black;
+  -webkit-box-shadow: 3px 3px 3px #888888;
+  -moz-box-shadow: 3px 3px 3px #888888;
+  box-shadow: 3px 3px 3px #888888;
+  margin-bottom: 5px;
+  margin-right: 5px;
+}
+
+.header { cursor: pointer; }
+
+.summary {
+  color: #777;
+  position: absolute;
+  top: 1px;
+  left: 120px;
+  width: 408px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  padding-bottom: 0.1em;
+}
+
+.date {
+  white-space: nowrap;
+  position: absolute;
+  top: 2px;
+  right: 5px;
+}
+
+.contents {
+  margin-left: 28px;
+  padding-top: 2px;
+  position: relative;
+}
+.contents p,
+.contents ul {
+  -webkit-margin-before: 0;
+  -webkit-margin-after: 0.3em;
+}
+
+.commentBox button {
+  margin-right: 3px;
+  margin-bottom: 1px;
+  padding: 1px;
+  text-align: center;
+  font-size: 8px;
+  font-weight: bold;
+  border: 1px solid black;
+  cursor: pointer;
+  color: #fff;
+  background-color: #4d90fe;
+  background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+  -webkit-border-radius: 2px;
+  -webkit-box-sizing: content-box;
+}
+.commentBox button div {
+  width: 25px;
+  white-space: nowrap;
+  color: #fff;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
new file mode 100644
index 0000000..9887dbf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.TextMarker.FromTo;
+
+public class CommentRange extends JavaScriptObject {
+  public static CommentRange create(int sl, int sc, int el, int ec) {
+    CommentRange r = createObject().cast();
+    r.set(sl, sc, el, ec);
+    return r;
+  }
+
+  public static CommentRange create(FromTo fromTo) {
+    if (fromTo == null) {
+      return null;
+    }
+
+    LineCharacter from = fromTo.getFrom();
+    LineCharacter to = fromTo.getTo();
+    return create(
+        from.getLine() + 1, from.getCh(),
+        to.getLine() + 1, to.getCh());
+  }
+
+  public final native int start_line() /*-{ return this.start_line; }-*/;
+  public final native int start_character() /*-{ return this.start_character; }-*/;
+  public final native int end_line() /*-{ return this.end_line; }-*/;
+  public final native int end_character() /*-{ return this.end_character; }-*/;
+
+  private final native void set(int sl, int sc, int el, int ec) /*-{
+    this.start_line = sl;
+    this.start_character = sc;
+    this.end_line = el;
+    this.end_character = ec;
+  }-*/;
+
+  protected CommentRange() {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
new file mode 100644
index 0000000..826d477
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class DiffApi {
+  public enum IgnoreWhitespace {
+    NONE, TRAILING, CHANGED, ALL;
+  };
+
+  public static void list(int id, String revision,
+      AsyncCallback<NativeMap<FileInfo>> cb) {
+    ChangeApi.revision(id, revision)
+      .view("files")
+      .get(NativeMap.copyKeysIntoChildren("path", cb));
+  }
+
+  public static DiffApi diff(PatchSet.Id id, String path) {
+    return new DiffApi(ChangeApi.revision(id)
+        .view("files").id(path)
+        .view("diff"));
+  }
+
+  private final RestApi call;
+
+  private DiffApi(RestApi call) {
+    this.call = call;
+  }
+
+  public DiffApi base(PatchSet.Id id) {
+    if (id != null) {
+      call.addParameter("base", id.get());
+    }
+    return this;
+  }
+
+  public DiffApi ignoreWhitespace(AccountDiffPreference.Whitespace w) {
+    switch (w) {
+      default:
+      case IGNORE_NONE:
+        return ignoreWhitespace(IgnoreWhitespace.NONE);
+      case IGNORE_SPACE_AT_EOL:
+        return ignoreWhitespace(IgnoreWhitespace.TRAILING);
+      case IGNORE_SPACE_CHANGE:
+        return ignoreWhitespace(IgnoreWhitespace.CHANGED);
+      case IGNORE_ALL_SPACE:
+        return ignoreWhitespace(IgnoreWhitespace.ALL);
+    }
+  }
+
+  public DiffApi ignoreWhitespace(IgnoreWhitespace w) {
+    if (w != null && w != IgnoreWhitespace.NONE) {
+      call.addParameter("ignore-whitespace", w);
+    }
+    return this;
+  }
+
+  public DiffApi intraline(boolean intraline) {
+    if (intraline) {
+      call.addParameterTrue("intraline");
+    }
+    return this;
+  }
+
+  public DiffApi wholeFile() {
+    call.addParameter("context", "ALL");
+    return this;
+  }
+
+  public DiffApi context(int lines) {
+    call.addParameter("context", lines);
+    return this;
+  }
+
+  public void get(AsyncCallback<DiffInfo> cb) {
+    call.get(cb);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
new file mode 100644
index 0000000..9b3ac38
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
@@ -0,0 +1,46 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+/** Object recording the position of a diff chunk and whether it's an edit */
+class DiffChunkInfo {
+  private DisplaySide side;
+  private int start;
+  private int end;
+  private boolean edit;
+
+  DiffChunkInfo(DisplaySide side, int start, int end, boolean edit) {
+    this.side = side;
+    this.start = start;
+    this.end = end;
+    this.edit = edit;
+  }
+
+  DisplaySide getSide() {
+    return side;
+  }
+
+  int getStart() {
+    return start;
+  }
+
+  int getEnd() {
+    return end;
+  }
+
+  boolean isEdit() {
+    return edit;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
new file mode 100644
index 0000000..f833509
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+
+public class DiffInfo extends JavaScriptObject {
+  public static final String GITLINK = "x-git/gitlink";
+  public static final String SYMLINK = "x-git/symlink";
+
+  public final native FileMeta meta_a() /*-{ return this.meta_a; }-*/;
+  public final native FileMeta meta_b() /*-{ return this.meta_b; }-*/;
+  public final native JsArrayString diff_header() /*-{ return this.diff_header; }-*/;
+  public final native JsArray<Region> content() /*-{ return this.content; }-*/;
+
+  public final ChangeType change_type() {
+    return ChangeType.valueOf(change_typeRaw());
+  }
+  private final native String change_typeRaw()
+  /*-{ return this.change_type }-*/;
+
+  public final IntraLineStatus intraline_status() {
+    String s = intraline_statusRaw();
+    return s != null
+        ? IntraLineStatus.valueOf(s)
+        : IntraLineStatus.OFF;
+  }
+  private final native String intraline_statusRaw()
+  /*-{ return this.intraline_status }-*/;
+
+  public final boolean has_skip() {
+    JsArray<Region> c = content();
+    for (int i = 0; i < c.length(); i++) {
+      if (c.get(i).skip() != 0) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public final String text_a() {
+    StringBuilder s = new StringBuilder();
+    JsArray<Region> c = content();
+    for (int i = 0; i < c.length(); i++) {
+      Region r = c.get(i);
+      if (r.ab() != null) {
+        append(s, r.ab());
+      } else if (r.a() != null) {
+        append(s, r.a());
+      }
+      // TODO skip may need to be handled
+    }
+    return s.toString();
+  }
+
+  public final String text_b() {
+    StringBuilder s = new StringBuilder();
+    JsArray<Region> c = content();
+    for (int i = 0; i < c.length(); i++) {
+      Region r = c.get(i);
+      if (r.ab() != null) {
+        append(s, r.ab());
+      } else if (r.b() != null) {
+        append(s, r.b());
+      }
+      // TODO skip may need to be handled
+    }
+    return s.toString();
+  }
+
+  private static void append(StringBuilder s, JsArrayString lines) {
+    for (int i = 0; i < lines.length(); i++) {
+      s.append(lines.get(i)).append('\n');
+    }
+  }
+
+  protected DiffInfo() {
+  }
+
+  public enum IntraLineStatus {
+    OFF, OK, TIMEOUT, FAILURE;
+  }
+
+  public static class FileMeta extends JavaScriptObject {
+    public final native String name() /*-{ return this.name; }-*/;
+    public final native String content_type() /*-{ return this.content_type; }-*/;
+
+    protected FileMeta() {
+    }
+  }
+
+  public static class Region extends JavaScriptObject {
+    public final native JsArrayString ab() /*-{ return this.ab; }-*/;
+    public final native JsArrayString a() /*-{ return this.a; }-*/;
+    public final native JsArrayString b() /*-{ return this.b; }-*/;
+    public final native int skip() /*-{ return this.skip || 0; }-*/;
+
+    public final native JsArray<Span> edit_a() /*-{ return this.edit_a }-*/;
+    public final native JsArray<Span> edit_b() /*-{ return this.edit_b }-*/;
+
+    protected Region() {
+    }
+  }
+
+  public static class Span extends JavaScriptObject {
+    public final native int skip() /*-{ return this[0]; }-*/;
+    public final native int mark() /*-{ return this[1]; }-*/;
+
+    protected Span() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
new file mode 100644
index 0000000..23af720
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -0,0 +1,143 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.dom.client.Element;
+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.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * A table with one row and two columns to hold the two CodeMirrors displaying
+ * the files to be diffed.
+ */
+class DiffTable extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, DiffTable> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface DiffTableStyle extends CssResource {
+    String intralineBg();
+    String diff();
+    String activeLine();
+    String range();
+    String rangeHighlight();
+    String showtabs();
+  }
+
+  @UiField
+  Element cmA;
+
+  @UiField
+  Element cmB;
+
+  @UiField
+  SidePanel sidePanel;
+
+  @UiField
+  Element patchSetNavRow;
+
+  @UiField
+  Element patchSetNavCellA;
+
+  @UiField
+  Element patchSetNavCellB;
+
+  @UiField(provided = true)
+  PatchSetSelectBox2 patchSetSelectBoxA;
+
+  @UiField(provided = true)
+  PatchSetSelectBox2 patchSetSelectBoxB;
+
+  @UiField
+  Element fileCommentRow;
+
+  @UiField
+  Element fileCommentCellA;
+
+  @UiField
+  Element fileCommentCellB;
+
+  @UiField(provided = true)
+  FileCommentPanel fileCommentPanelA;
+
+  @UiField(provided = true)
+  FileCommentPanel fileCommentPanelB;
+
+  @UiField
+  static DiffTableStyle style;
+
+  private SideBySide2 host;
+
+  DiffTable(SideBySide2 host, PatchSet.Id base, PatchSet.Id revision, String path) {
+    patchSetSelectBoxA = new PatchSetSelectBox2(
+        this, DisplaySide.A, revision.getParentKey(), base, path);
+    patchSetSelectBoxB = new PatchSetSelectBox2(
+        this, DisplaySide.B, revision.getParentKey(), revision, path);
+    PatchSetSelectBox2.link(patchSetSelectBoxA, patchSetSelectBoxB);
+    fileCommentPanelA = new FileCommentPanel(host, this, path, DisplaySide.A);
+    fileCommentPanelB = new FileCommentPanel(host, this, path, DisplaySide.B);
+    initWidget(uiBinder.createAndBindUi(this));
+    this.host = host;
+  }
+
+  void updateFileCommentVisibility(boolean forceHide) {
+    UIObject.setVisible(patchSetNavRow, !forceHide);
+    if (forceHide || (fileCommentPanelA.getBoxCount() == 0 &&
+        fileCommentPanelB.getBoxCount() == 0)) {
+      UIObject.setVisible(fileCommentRow, false);
+    } else {
+      UIObject.setVisible(fileCommentRow, true);
+    }
+    host.resizeCodeMirror();
+  }
+
+  private FileCommentPanel getPanelFromSide(DisplaySide side) {
+    return side == DisplaySide.A ? fileCommentPanelA : fileCommentPanelB;
+  }
+
+  void createOrEditFileComment(DisplaySide side) {
+    getPanelFromSide(side).createOrEditFileComment();
+    updateFileCommentVisibility(false);
+  }
+
+  void addFileCommentBox(CommentBox box) {
+    getPanelFromSide(box.getSide()).addFileComment(box);
+  }
+
+  void onRemoveDraftBox(DraftBox box) {
+    getPanelFromSide(box.getSide()).onRemoveDraftBox(box);
+  }
+
+  int getHeaderHeight() {
+    return fileCommentRow.getOffsetHeight() + patchSetSelectBoxA.getOffsetHeight();
+  }
+
+  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo info) {
+    patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a());
+    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b());
+  }
+
+  void add(Widget widget) {
+    ((HTMLPanel) getWidget()).add(widget);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
new file mode 100644
index 0000000..b757dd1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:d='urn:import:com.google.gerrit.client.diff'>
+  <ui:style type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
+    @external .CodeMirror, .CodeMirror-lines, .CodeMirror-selectedtext;
+    @external .CodeMirror-linenumber, .CodeMirror-vscrollbar .CodeMirror-scroll;
+    @external .cm-keymap-fat-cursor, CodeMirror-cursor;
+    @external .cm-searching, .cm-trailingspace, .cm-tab;
+
+    .difftable { max-width: 1484px; }
+    .difftable .CodeMirror-lines { padding: 0; }
+    .difftable .CodeMirror pre {
+      padding: 0;
+      padding-bottom: 0.11em;
+      overflow: hidden;
+      border-right: 0;
+      width: auto;
+    }
+    .difftable .CodeMirror pre span {
+      padding-bottom: 0.11em;
+    }
+    .contentCell {
+      padding: 0;
+    }
+    .sidePanelCell {
+      padding: 0;
+      width: 10px;
+    }
+    .table {
+      width: 100%;
+      table-layout: fixed;
+      border-spacing: 0;
+    }
+
+    .a, .b {
+      padding: 0;
+      width: 50%;
+    }
+
+    /* Hide left side scrollbar, right side controls both views. */
+    .a .CodeMirror-scroll { padding-right: 0; }
+    .a .CodeMirror-vscrollbar { display: none !important; }
+
+    .a .diff { background-color: #faa; }
+    .b .diff { background-color: #9f9; }
+    .a .intralineBg { background-color: #fee; }
+    .b .intralineBg { background-color: #dfd; }
+
+    .fileCommentRow {
+      background-color: #eee;
+      line-height: 1;
+    }
+    .fileCommentCell {
+      overflow-x: auto;
+    }
+
+    .activeLine .CodeMirror-linenumber {
+      background-color: #bcf !important;
+      color: #000;
+    }
+
+    .range {
+      background-color: #ffd500 !important;
+    }
+    .rangeHighlight {
+      background-color: #ffff00 !important;
+    }
+    .cm-searching {
+      background-color: #ffa !important;
+    }
+    .cm-trailingspace {
+      background-color: red !important;
+    }
+    .difftable .CodeMirror-selectedtext {
+      background-color: inherit !important;
+    }
+    .difftable .CodeMirror-linenumber {
+      height: 1.11em;
+      cursor: pointer;
+    }
+    .difftable .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
+      background: transparent;
+      text-decoration: underline;
+    }
+    .showtabs .cm-tab:before {
+      content: "\00bb";
+      color: #f00;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.difftable}'>
+    <table class='{style.table}'>
+      <tr>
+        <td class='{style.contentCell}'>
+          <table class='{style.table}'>
+            <tr ui:field='patchSetNavRow' class='{style.fileCommentRow}'>
+              <td ui:field='patchSetNavCellA'>
+                <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxA' />
+              </td>
+              <td ui:field='patchSetNavCellB'>
+                <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxB' />
+              </td>
+            </tr>
+            <tr ui:field='fileCommentRow' class='{style.fileCommentRow}'>
+              <td ui:field='fileCommentCellA' class='{style.fileCommentCell}'>
+                <d:FileCommentPanel ui:field='fileCommentPanelA' />
+              </td>
+              <td ui:field='fileCommentCellB' class='{style.fileCommentCell}'>
+                <d:FileCommentPanel ui:field='fileCommentPanelB' />
+              </td>
+            </tr>
+            <tr>
+              <td ui:field='cmA' class='{style.a}'></td>
+              <td ui:field='cmB' class='{style.b}'></td>
+            </tr>
+          </table>
+        </td>
+        <td class='{style.sidePanelCell}'><d:SidePanel ui:field='sidePanel'/></td>
+      </tr>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
new file mode 100644
index 0000000..c51a673
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+/** Enum representing the side on a side-by-side view */
+enum DisplaySide {
+  A, B;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
new file mode 100644
index 0000000..d54df3a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -0,0 +1,372 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.CommentInput;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+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.Timer;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import net.codemirror.lib.CodeMirror;
+
+/** An HtmlPanel for displaying and editing a draft */
+class DraftBox extends CommentBox {
+  interface Binder extends UiBinder<HTMLPanel, DraftBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private static final int INITIAL_LINES = 5;
+  private static final int MAX_LINES = 30;
+
+  private final SideBySide2 parent;
+  private final CommentLinkProcessor linkProcessor;
+  private final PatchSet.Id psId;
+  private CommentInfo comment;
+  private PublishedBox replyToBox;
+  private Timer expandTimer;
+
+  @UiField Widget header;
+  @UiField Element summary;
+  @UiField Element date;
+
+  @UiField Element p_view;
+  @UiField HTML message;
+  @UiField Button edit;
+  @UiField Button discard1;
+
+  @UiField Element p_edit;
+  @UiField NpTextArea editArea;
+  @UiField Button save;
+  @UiField Button cancel;
+  @UiField Button discard2;
+
+  DraftBox(
+      SideBySide2 sideBySide,
+      CodeMirror cm,
+      DisplaySide side,
+      CommentLinkProcessor clp,
+      PatchSet.Id id,
+      CommentInfo info) {
+    super(cm, info, side);
+
+    parent = sideBySide;
+    linkProcessor = clp;
+    psId = id;
+    initWidget(uiBinder.createAndBindUi(this));
+
+    expandTimer = new Timer() {
+      @Override
+      public void run() {
+        expandText();
+      }
+    };
+    set(info);
+
+    header.addDomHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        if (!isEdit()) {
+          setOpen(!isOpen());
+        }
+      }
+    }, ClickEvent.getType());
+    addDomHandler(new DoubleClickHandler() {
+      @Override
+      public void onDoubleClick(DoubleClickEvent event) {
+        if (isEdit()) {
+          editArea.setFocus(true);
+        } else {
+          setOpen(true);
+          setEdit(true);
+        }
+      }
+    }, DoubleClickEvent.getType());
+    addDomHandler(new MouseMoveHandler() {
+      @Override
+      public void onMouseMove(MouseMoveEvent event) {
+        resizePaddingWidget();
+      }
+    }, MouseMoveEvent.getType());
+  }
+
+  private void set(CommentInfo info) {
+    date.setInnerText(FormatUtil.shortFormatDayTime(info.updated()));
+    if (info.message() != null) {
+      String msg = info.message().trim();
+      summary.setInnerText(msg);
+      message.setHTML(linkProcessor.apply(
+          new SafeHtmlBuilder().append(msg).wikify()));
+    }
+    comment = info;
+  }
+
+  @Override
+  CommentInfo getCommentInfo() {
+    return comment;
+  }
+
+  @Override
+  boolean isOpen() {
+    return UIObject.isVisible(p_view);
+  }
+
+  @Override
+  void setOpen(boolean open) {
+    UIObject.setVisible(summary, !open);
+    UIObject.setVisible(p_view, open);
+    super.setOpen(open);
+  }
+
+  private void expandText() {
+    double cols = editArea.getCharacterWidth();
+    int rows = 2;
+    for (String line : editArea.getValue().split("\n")) {
+      rows += Math.ceil((1.0 + line.length()) / cols);
+    }
+    rows = Math.max(INITIAL_LINES, Math.min(rows, MAX_LINES));
+    if (editArea.getVisibleLines() != rows) {
+      editArea.setVisibleLines(rows);
+    }
+    resizePaddingWidget();
+  }
+
+  private boolean isEdit() {
+    return UIObject.isVisible(p_edit);
+  }
+
+  void setEdit(boolean edit) {
+    UIObject.setVisible(summary, false);
+    UIObject.setVisible(p_view, !edit);
+    UIObject.setVisible(p_edit, edit);
+
+    setRangeHighlight(edit);
+    if (edit) {
+      final String msg = comment.message() != null
+          ? comment.message().trim()
+          : "";
+      editArea.setValue(msg);
+      editArea.setFocus(true);
+      cancel.setVisible(!isNew());
+      expandText();
+      if (msg.length() > 0) {
+        Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+          @Override
+          public boolean execute() {
+            editArea.setCursorPos(msg.length());
+            return false;
+          }
+        }, 0);
+      }
+    } else {
+      expandTimer.cancel();
+    }
+    resizePaddingWidget();
+  }
+
+  void registerReplyToBox(PublishedBox box) {
+    replyToBox = box;
+  }
+
+  @Override
+  protected void onUnload() {
+    expandTimer.cancel();
+    super.onUnload();
+  }
+
+  private void removeUI() {
+    if (replyToBox != null) {
+      replyToBox.unregisterReplyBox();
+    }
+    clearRange();
+    setRangeHighlight(false);
+    removeFromParent();
+    if (!getCommentInfo().has_line()) {
+      parent.removeFileCommentBox(this);
+      return;
+    }
+    PaddingManager manager = getPaddingManager();
+    manager.remove(this);
+    parent.removeDraft(this, comment.line() - 1);
+    getCm().focus();
+    getSelfWidgetWrapper().getWidget().clear();
+    getGutterWrapper().remove();
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        resizePaddingWidget();
+      }
+    });
+  }
+
+  @UiHandler("message")
+  void onMessageClick(ClickEvent e) {
+    e.stopPropagation();
+  }
+
+  @UiHandler("message")
+  void onMessageDoubleClick(DoubleClickEvent e) {
+    setEdit(true);
+  }
+
+  @UiHandler("edit")
+  void onEdit(ClickEvent e) {
+    e.stopPropagation();
+    setEdit(true);
+  }
+
+  @UiHandler("save")
+  void onSave(ClickEvent e) {
+    e.stopPropagation();
+    onSave();
+  }
+
+  private void onSave() {
+    String message = editArea.getValue().trim();
+    if (message.length() == 0) {
+      return;
+    }
+
+    CommentInfo original = comment;
+    CommentInput input = CommentInput.create(original);
+    input.setMessage(message);
+    enableEdit(false);
+
+    GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
+      @Override
+      public void onSuccess(CommentInfo result) {
+        enableEdit(true);
+        set(result);
+        if (result.message().length() < 70) {
+          UIObject.setVisible(p_edit, false);
+          setOpen(false);
+        } else {
+          setEdit(false);
+        }
+      }
+
+      @Override
+      public void onFailure(Throwable e) {
+        enableEdit(true);
+        super.onFailure(e);
+      }
+    };
+    if (original.id() == null) {
+      CommentApi.createDraft(psId, input, cb);
+    } else {
+      CommentApi.updateDraft(psId, original.id(), input, cb);
+    }
+    getCm().focus();
+  }
+
+  private void enableEdit(boolean on) {
+    editArea.setEnabled(on);
+    save.setEnabled(on);
+    cancel.setEnabled(on);
+    discard2.setEnabled(on);
+  }
+
+  @UiHandler("cancel")
+  void onCancel(ClickEvent e) {
+    e.stopPropagation();
+    if (isNew() && !isDirty()) {
+      removeUI();
+    } else {
+      setEdit(false);
+      getCm().focus();
+    }
+  }
+
+  @UiHandler({"discard1", "discard2"})
+  void onDiscard(ClickEvent e) {
+    e.stopPropagation();
+    if (isNew()) {
+      removeUI();
+    } else {
+      setEdit(false);
+      CommentApi.deleteDraft(psId, comment.id(),
+          new GerritCallback<JavaScriptObject>() {
+        @Override
+        public void onSuccess(JavaScriptObject result) {
+          removeUI();
+        }
+      });
+    }
+  }
+
+  @UiHandler("editArea")
+  void onKeyDown(KeyDownEvent e) {
+    if ((e.isControlKeyDown() || e.isMetaKeyDown())
+        && !e.isAltKeyDown() && !e.isShiftKeyDown()) {
+      switch (e.getNativeKeyCode()) {
+        case 's':
+        case 'S':
+          e.preventDefault();
+          onSave();
+          return;
+      }
+    } else if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE && !isDirty()) {
+      if (isNew()) {
+        removeUI();
+        return;
+      } else {
+        setEdit(false);
+        getCm().focus();
+        return;
+      }
+    }
+    expandTimer.schedule(250);
+  }
+
+  private boolean isNew() {
+    return comment.id() == null;
+  }
+
+  private boolean isDirty() {
+    String msg = editArea.getValue().trim();
+    if (isNew()) {
+      return msg.length() > 0;
+    }
+    return !msg.equals(comment.message() != null
+        ? comment.message().trim()
+        : "");
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
new file mode 100644
index 0000000..52ef0ff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gerrit.client'
+    xmlns:e='urn:import:com.google.gwtexpui.globalkey.client'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
+  <ui:style>
+    .draft {
+      width: 45px;
+      text-align: center;
+      color: #fff;
+      background-color: #aaa;
+      -webkit-border-radius: 2px;
+    }
+    .editArea { max-width: 637px; }
+    button.button div {
+      width: 35px;
+    }
+    button.discard {
+      color: #d14836;
+      background-color: #d14836;
+      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
+      position: absolute;
+      left: 150px;
+    }
+  </ui:style>
+
+  <g:HTMLPanel styleName='{res.style.commentBox}'>
+    <div class='{res.style.contents}'>
+      <g:HTMLPanel ui:field='header' styleName='{res.style.header}'>
+        <div class='{style.draft}'>Draft</div>
+        <div ui:field='summary' class='{res.style.summary}'/>
+        <div ui:field='date' class='{res.style.date}'/>
+      </g:HTMLPanel>
+      <div ui:field='p_view' aria-hidden='true' style='display: NONE'>
+        <g:HTML ui:field='message' styleName=''/>
+        <div style='position: relative'>
+          <g:Button ui:field='edit'
+              title='Edit this draft comment'
+              styleName='{style.button}'>
+            <ui:attribute name='title'/>
+            <div><ui:msg>Edit</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='discard1'
+              title='Discard this draft comment'
+              styleName='{style.button}'
+              addStyleNames='{style.discard}'>
+            <ui:attribute name='title'/>
+            <div><ui:msg>Discard</ui:msg></div>
+          </g:Button>
+        </div>
+      </div>
+      <div ui:field='p_edit' aria-hidden='true' style='display: NONE'>
+        <e:NpTextArea ui:field='editArea'
+            characterWidth='60'
+            visibleLines='5'
+            spellCheck='true'
+            styleName='{style.editArea}'/>
+        <div style='position: relative'>
+          <g:Button ui:field='save'
+              title='Save this draft comment'
+              styleName='{style.button}'>
+            <ui:attribute name='title'/>
+            <div><ui:msg>Save</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='cancel' styleName='{style.button}'>
+            <div><ui:msg>Cancel</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='discard2'
+              title='Discard this draft comment'
+              styleName='{style.button}'
+              addStyleNames='{style.discard}'>
+            <ui:attribute name='title'/>
+            <div><ui:msg>Discard</ui:msg></div>
+          </g:Button>
+        </div>
+      </div>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
new file mode 100644
index 0000000..246c101
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.JsArrayString;
+
+import net.codemirror.lib.LineCharacter;
+
+/** An iterator for intraline edits */
+class EditIterator {
+  private final JsArrayString lines;
+  private final int startLine;
+  private int currLineIndex;
+  private int currLineOffset;
+
+  EditIterator(JsArrayString lineArray, int start) {
+    lines = lineArray;
+    startLine = start;
+  }
+
+  LineCharacter advance(int numOfChar) {
+    while (currLineIndex < lines.length()) {
+      int lengthWithNewline =
+          lines.get(currLineIndex).length() - currLineOffset + 1;
+      if (numOfChar < lengthWithNewline) {
+        LineCharacter at = LineCharacter.create(
+            startLine + currLineIndex,
+            numOfChar + currLineOffset);
+        currLineOffset += numOfChar;
+        return at;
+      }
+      numOfChar -= lengthWithNewline;
+      advanceLine();
+      if (numOfChar == 0) {
+        return LineCharacter.create(startLine + currLineIndex, 0);
+      }
+    }
+    throw new IllegalStateException("EditIterator index out of bound");
+  }
+
+  private void advanceLine() {
+    currLineIndex++;
+    currLineOffset = 0;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java
new file mode 100644
index 0000000..f252e65
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java
@@ -0,0 +1,84 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * HTMLPanel to hold file comments.
+ * TODO: Need to resize CodeMirror if this is resized since we don't have the
+ * system scrollbar.
+ */
+class FileCommentPanel extends Composite {
+
+  private SideBySide2 parent;
+  private DiffTable table;
+  private String path;
+  private DisplaySide side;
+  private List<CommentBox> boxes;
+  private FlowPanel body;
+
+  FileCommentPanel(SideBySide2 host, DiffTable table, String path, DisplaySide side) {
+    this.parent = host;
+    this.table = table;
+    this.path = path;
+    this.side = side;
+    boxes = new ArrayList<CommentBox>();
+    initWidget(body = new FlowPanel());
+  }
+
+  void createOrEditFileComment() {
+    if (!Gerrit.isSignedIn()) {
+      Gerrit.doSignIn(parent.getToken());
+      return;
+    }
+    if (boxes.isEmpty()) {
+      CommentInfo info = CommentInfo.createFile(
+          path,
+          parent.getStoredSideFromDisplaySide(side),
+          null,
+          null);
+      addFileComment(parent.addDraftBox(info, side));
+    } else {
+      CommentBox box = boxes.get(boxes.size() - 1);
+      if (box instanceof DraftBox) {
+        ((DraftBox) box).setEdit(true);
+      } else {
+        addFileComment(((PublishedBox) box).addReplyBox());
+      }
+    }
+  }
+
+  int getBoxCount() {
+    return boxes.size();
+  }
+
+  void addFileComment(CommentBox box) {
+    boxes.add(box);
+    body.add(box);
+    table.updateFileCommentVisibility(false);
+  }
+
+  void onRemoveDraftBox(DraftBox box) {
+    boxes.remove(box);
+    table.updateFileCommentVisibility(false);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
new file mode 100644
index 0000000..dc52aa3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+public class FileInfo extends JavaScriptObject {
+  public final native String path() /*-{ return this.path; }-*/;
+  public final native String old_path() /*-{ return this.old_path; }-*/;
+  public final native int lines_inserted() /*-{ return this.lines_inserted || 0; }-*/;
+  public final native int lines_deleted() /*-{ return this.lines_deleted || 0; }-*/;
+  public final native boolean binary() /*-{ return this.binary || false; }-*/;
+  public final native String status() /*-{ return this.status; }-*/;
+
+  public final native int _row() /*-{ return this._row }-*/;
+  public final native void _row(int r) /*-{ this._row = r }-*/;
+
+  public static void sortFileInfoByPath(JsArray<FileInfo> list) {
+    Collections.sort(Natives.asList(list), new Comparator<FileInfo>() {
+      @Override
+      public int compare(FileInfo a, FileInfo b) {
+        if (Patch.COMMIT_MSG.equals(a.path())) {
+          return -1;
+        } else if (Patch.COMMIT_MSG.equals(b.path())) {
+          return 1;
+        }
+        return a.path().compareTo(b.path());
+      }
+    });
+  }
+
+  public static String getFileName(String path) {
+    String fileName = Patch.COMMIT_MSG.equals(path)
+        ? Util.C.commitMessage()
+        : path;
+    int s = fileName.lastIndexOf('/');
+    return s >= 0 ? fileName.substring(s + 1) : fileName;
+  }
+
+  protected FileInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
new file mode 100644
index 0000000..2d4616f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -0,0 +1,217 @@
+// 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.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ReviewInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtorm.client.KeyUtil;
+
+class Header extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, Header> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField CheckBox reviewed;
+  @UiField Element filePath;
+
+  @UiField Element noDiff;
+
+  @UiField InlineHyperlink prev;
+  @UiField InlineHyperlink up;
+  @UiField InlineHyperlink next;
+
+  private final KeyCommandSet keys;
+  private final PatchSet.Id base;
+  private final PatchSet.Id patchSetId;
+  private final String path;
+  private boolean hasPrev;
+  private boolean hasNext;
+  private String nextPath;
+
+  Header(KeyCommandSet keys, PatchSet.Id base, PatchSet.Id patchSetId,
+      String path) {
+    initWidget(uiBinder.createAndBindUi(this));
+    this.keys = keys;
+    this.base = base;
+    this.patchSetId = patchSetId;
+    this.path = path;
+
+    SafeHtml.setInnerHTML(filePath, formatPath(path));
+    up.setTargetHistoryToken(PageLinks.toChange(
+        patchSetId.getParentKey(),
+        String.valueOf(patchSetId.get())));
+  }
+
+  private static SafeHtml formatPath(String path) {
+    SafeHtmlBuilder b = new SafeHtmlBuilder();
+    if (Patch.COMMIT_MSG.equals(path)) {
+      return b.append(Util.C.commitMessage());
+    }
+
+    int s = path.lastIndexOf('/') + 1;
+    b.append(path.substring(0, s));
+    b.openElement("b");
+    b.append(path.substring(s));
+    b.closeElement("b");
+    return b;
+  }
+
+  @Override
+  protected void onLoad() {
+    ChangeApi.revision(patchSetId).view("files").get(
+        new GerritCallback<NativeMap<FileInfo>>() {
+      @Override
+      public void onSuccess(NativeMap<FileInfo> result) {
+        result.copyKeysIntoChildren("path");
+        JsArray<FileInfo> files = result.values();
+        FileInfo.sortFileInfoByPath(files);
+        int index = 0; // TODO: Maybe use patchIndex.
+        for (int i = 0; i < files.length(); i++) {
+          if (path.equals(files.get(i).path())) {
+            index = i;
+            break;
+          }
+        }
+        FileInfo nextInfo = index == files.length() - 1
+            ? null
+            : files.get(index + 1);
+        setupNav(prev, '[', PatchUtil.C.previousFileHelp(),
+            index == 0 ? null : files.get(index - 1));
+        setupNav(next, ']', PatchUtil.C.nextFileHelp(), nextInfo);
+        nextPath = nextInfo != null ? nextInfo.path() : null;
+      }
+    });
+
+    if (Gerrit.isSignedIn()) {
+      ChangeApi.revision(patchSetId).view("files")
+        .addParameterTrue("reviewed")
+        .get(new AsyncCallback<JsArrayString>() {
+            @Override
+            public void onSuccess(JsArrayString result) {
+              for (int i = 0; i < result.length(); i++) {
+                if (path.equals(result.get(i))) {
+                  reviewed.setValue(true, false);
+                  break;
+                }
+              }
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          });
+    }
+  }
+
+  void setReviewed(boolean r) {
+    reviewed.setValue(r, true);
+  }
+
+  boolean isReviewed() {
+    return reviewed.getValue();
+  }
+
+  @UiHandler("reviewed")
+  void onValueChange(ValueChangeEvent<Boolean> event) {
+    RestApi api = ChangeApi.revision(patchSetId)
+        .view("files")
+        .id(path)
+        .view("reviewed");
+    if (event.getValue()) {
+      api.put(CallbackGroup.<ReviewInfo>emptyCallback());
+    } else {
+      api.delete(CallbackGroup.<ReviewInfo>emptyCallback());
+    }
+  }
+
+  private String url(FileInfo info) {
+    Change.Id c = patchSetId.getParentKey();
+    StringBuilder p = new StringBuilder();
+    p.append("/c/").append(c).append('/');
+    if (base != null) {
+      p.append(base.get()).append("..");
+    }
+    p.append(patchSetId.get()).append('/').append(KeyUtil.encode(info.path()));
+    p.append(info.binary() ? ",unified" : ",cm");
+    return p.toString();
+  }
+
+  private void setupNav(InlineHyperlink link, int key, String help, FileInfo info) {
+    if (info != null) {
+      final String url = url(info);
+      link.setTargetHistoryToken(url);
+      link.setTitle(FileInfo.getFileName(info.path()));
+      keys.add(new KeyCommand(0, key, help) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          Gerrit.display(url);
+        }
+      });
+      if (link == prev) {
+        hasPrev = true;
+      } else {
+        hasNext = true;
+      }
+    } else {
+      link.getElement().getStyle().setVisibility(Visibility.HIDDEN);
+      keys.add(new UpToChangeCommand2(patchSetId, 0, key));
+    }
+  }
+
+  boolean hasPrev() {
+    return hasPrev;
+  }
+
+  boolean hasNext() {
+    return hasNext;
+  }
+
+  String getNextPath() {
+    return nextPath;
+  }
+
+  void removeNoDiff() {
+    noDiff.removeFromParent();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
new file mode 100644
index 0000000..18e7d97
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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>
+  .header {
+    position: relative;
+    max-width: 1484px;
+  }
+  .reviewed input {
+    margin: 0;
+    padding: 0;
+    vertical-align: bottom;
+  }
+  .path {
+  }
+  .navigation {
+    position: absolute;
+    top: 0;
+    right: 15px;
+    font-family: Arial Unicode MS, sans-serif;
+  }
+  .nodiff {
+    position: absolute;
+    left: literal("calc(50% - 30px)");
+    color: #B00000;
+  }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.header}'>
+    <g:CheckBox ui:field='reviewed'
+        styleName='{style.reviewed}'
+        title='Mark file as reviewed (Shortcut: r)'>
+      <ui:attribute name='title'/>
+    </g:CheckBox>
+    <span ui:field='filePath' class='{style.path}'/>
+
+    <span ui:field='noDiff' class='{style.nodiff}'>
+      <ui:msg>No Differences</ui:msg>
+    </span>
+
+    <div class='{style.navigation}'>
+      <x:InlineHyperlink ui:field='prev'>&#x21e6;</x:InlineHyperlink>
+      <x:InlineHyperlink ui:field='up' title='Up to change (Shortcut: u)'>
+        <ui:attribute name='title'/>
+        &#x21e7;
+      </x:InlineHyperlink>
+      <x:InlineHyperlink ui:field='next'>&#x21e8;</x:InlineHyperlink>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
new file mode 100644
index 0000000..7a5ce5d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
@@ -0,0 +1,184 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Helper class to handle calculations involving line gaps. */
+class LineMapper {
+  private int lineA;
+  private int lineB;
+  private List<LineGap> lineMapAtoB;
+  private List<LineGap> lineMapBtoA;
+
+  LineMapper() {
+    lineMapAtoB = new ArrayList<LineGap>();
+    lineMapBtoA = new ArrayList<LineGap>();
+  }
+
+  int getLineA() {
+    return lineA;
+  }
+
+  int getLineB() {
+    return lineB;
+  }
+
+  void appendCommon(int numLines) {
+    lineA += numLines;
+    lineB += numLines;
+  }
+
+  void appendInsert(int numLines) {
+    int origLineB = lineB;
+    lineB += numLines;
+    int bAheadOfA = lineB - lineA;
+    lineMapAtoB.add(new LineGap(lineA, -1, bAheadOfA));
+    lineMapBtoA.add(new LineGap(origLineB, lineB - 1, -bAheadOfA));
+  }
+
+  void appendDelete(int numLines) {
+    int origLineA = lineA;
+    lineA += numLines;
+    int aAheadOfB = lineA - lineB;
+    lineMapAtoB.add(new LineGap(origLineA, lineA - 1, -aAheadOfB));
+    lineMapBtoA.add(new LineGap(lineB, -1, aAheadOfB));
+  }
+
+  /**
+   * Helper method to retrieve the line number on the other side.
+   *
+   * Given a line number on one side, performs a binary search in the lineMap
+   * to find the corresponding LineGap record.
+   *
+   * A LineGap records gap information from the start of an actual gap up to
+   * the start of the next gap. In the following example,
+   * lineMapAtoB will have LineGap: {start: 1, end: -1, delta: 3}
+   * (end set to -1 to represent a dummy gap of length zero. The binary search
+   * only looks at start so setting it to -1 has no effect here.)
+   * lineMapBtoA will have LineGap: {start: 1, end: 3, delta: -3}
+   * These LineGaps control lines between 1 and 5.
+   *
+   * The "delta" is computed as the number to add on our side to get the line
+   * number on the other side given a line after the actual gap, so the result
+   * will be (line + delta). All lines within the actual gap (1 to 3) are
+   * considered corresponding to the last line above the region on the other
+   * side, which is 0 in this case. For these lines, we do (end + delta).
+   *
+   * For example, to get the line number on the left corresponding to 1 on the
+   * right (lineOnOther(REVISION, 1)), the method looks up in lineMapBtoA,
+   * finds the "delta" to be -3, and returns 3 + (-3) = 0 since 1 falls in the
+   * actual gap. On the other hand, the line corresponding to 5 on the right
+   * will be 5 + (-3) = 2, since 5 is in the region after the gap (but still
+   * controlled by the current LineGap).
+   *
+   * PARENT REVISION
+   *   0   |   0
+   *   -   |   1 \                      \
+   *   -   |   2 | Actual insertion gap |
+   *   -   |   3 /                      | Region controlled by one LineGap
+   *   1   |   4   <- delta = 4 - 1 = 3 |
+   *   2   |   5                        /
+   *   -   |   6
+   *      ...
+   */
+  LineOnOtherInfo lineOnOther(DisplaySide mySide, int line) {
+    List<LineGap> lineGaps = mySide == DisplaySide.A ? lineMapAtoB : lineMapBtoA;
+    // Create a dummy LineGap for the search.
+    int ret = Collections.binarySearch(lineGaps, new LineGap(line));
+    if (ret == -1) {
+      return new LineOnOtherInfo(line, true);
+    } else {
+      LineGap lookup = lineGaps.get(0 <= ret ? ret : -ret - 2);
+      int start = lookup.start;
+      int end = lookup.end;
+      int delta = lookup.delta;
+      if (start <= line && line <= end && end != -1) { // Line falls within gap
+        return new LineOnOtherInfo(end + delta, false);
+      } else { // Line after gap
+        return new LineOnOtherInfo(line + delta, true);
+      }
+    }
+  }
+
+  /**
+   * @field line The line number on the other side.
+   * @field aligned Whether the two lines are at the same height when displayed.
+   */
+  static class LineOnOtherInfo {
+    private int line;
+    private boolean aligned;
+
+    LineOnOtherInfo(int line, boolean aligned) {
+      this.line = line;
+      this.aligned = aligned;
+    }
+
+    int getLine() {
+      return line;
+    }
+
+    boolean isAligned() {
+      return aligned;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof LineOnOtherInfo) {
+        LineOnOtherInfo other = (LineOnOtherInfo) obj;
+        return aligned == other.aligned && line == other.line;
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return line + " " + aligned;
+    }
+  }
+
+  /**
+   * Helper class to record line gap info and assist in calculation of line
+   * number on the other side.
+   *
+   * For a mapping from A to B, where A is the side with an insertion:
+   * @field start The start line of the insertion in A.
+   * @field end The exclusive end line of the insertion in A.
+   * @field delta The offset added to A to get the line number in B calculated
+   *              from end.
+   */
+  private static class LineGap implements Comparable<LineGap> {
+    private final int start;
+    private final int end;
+    private final int delta;
+
+    private LineGap(int start, int end, int delta) {
+      this.start = start;
+      this.end = end;
+      this.delta = delta;
+    }
+
+    private LineGap(int line) {
+      this(line, 0, 0);
+    }
+
+    @Override
+    public int compareTo(LineGap o) {
+      return start - o.start;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
new file mode 100644
index 0000000..c22769e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+
+/** A KeyCommand that does nothing, used to display a help message */
+class NoOpKeyCommand extends KeyCommand {
+  NoOpKeyCommand(int mask, int key, String help) {
+    super(mask, key, help);
+  }
+
+  @Override
+  public void onKeyPress(KeyPressEvent event) {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java
new file mode 100644
index 0000000..dc212e5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.dom.client.Element;
+
+import net.codemirror.lib.LineWidget;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages paddings for CommentBoxes. Each line that may need to be padded owns
+ * a PaddingManager instance, which maintains a padding widget whose height
+ * changes as necessary. PaddingManager calculates padding by taking the
+ * difference of the sum of CommentBox heights on the two sides.
+ *
+ * Note that in the case of an insertion or deletion gap, A PaddingManager
+ * can map to a list of managers on the other side. The padding needed is then
+ * calculated from the sum of all their heights.
+ *
+ * TODO: Let PaddingManager also take care of the paddings introduced by
+ * insertions and deletions.
+ */
+class PaddingManager {
+  private List<CommentBox> comments;
+  private PaddingWidgetWrapper wrapper;
+  private List<PaddingManager> others;
+
+  PaddingManager(PaddingWidgetWrapper padding) {
+    comments = new ArrayList<CommentBox>();
+    others = new ArrayList<PaddingManager>();
+    this.wrapper = padding;
+  }
+
+  static void link(PaddingManager a, PaddingManager b) {
+    if (!a.others.contains(b)) {
+      a.others.add(b);
+    }
+    if (!b.others.contains(a)) {
+      b.others.add(a);
+    }
+  }
+
+  private int getMyTotalHeight() {
+    int total = 0;
+    for (CommentBox box : comments) {
+      /**
+       * This gets the height of CM's line widget div, taking the margin and
+       * the horizontal scrollbar into account.
+       */
+      total += box.getSelfWidgetWrapper().getElement().getParentElement().getOffsetHeight();
+    }
+    return total;
+  }
+
+  /**
+   * If this instance is on the insertion side, its counterpart on the other
+   * side will map to a group of PaddingManagers on this side, so we calculate
+   * the group's total height instead of an individual one's.
+   */
+  private int getGroupTotalHeight() {
+    if (others.size() > 1) {
+      return getMyTotalHeight();
+    } else {
+      return others.get(0).getOthersTotalHeight();
+    }
+  }
+
+  private int getOthersTotalHeight() {
+    int total = 0;
+    for (PaddingManager manager : others) {
+      total += manager.getMyTotalHeight();
+    }
+    return total;
+  }
+
+  private void setPaddingHeight(int height) {
+    SideBySide2.setHeightInPx(wrapper.element, height);
+    wrapper.widget.changed();
+  }
+
+  void resizePaddingWidget() {
+    if (others.isEmpty()) {
+      return;
+    }
+    int myHeight = getGroupTotalHeight();
+    int othersHeight = getOthersTotalHeight();
+    int paddingNeeded = othersHeight - myHeight;
+    if (paddingNeeded < 0) {
+      for (PaddingManager manager : others.get(0).others) {
+        manager.setPaddingHeight(0);
+      }
+      others.get(others.size() - 1).setPaddingHeight(-paddingNeeded);
+    } else {
+      setPaddingHeight(paddingNeeded);
+      for (PaddingManager other : others) {
+        other.setPaddingHeight(0);
+      }
+    }
+  }
+
+  /** This is unused now because threading info is ignored. */
+  int getReplyIndex(CommentBox box) {
+    return comments.indexOf(box) + 1;
+  }
+
+  int getCurrentCount() {
+    return comments.size();
+  }
+
+  void insert(CommentBox box, int index) {
+    comments.add(index, box);
+  }
+
+  void remove(CommentBox box) {
+    comments.remove(box);
+  }
+
+  static class PaddingWidgetWrapper {
+    private LineWidget widget;
+    private Element element;
+
+    PaddingWidgetWrapper(LineWidget w, Element e) {
+      widget = w;
+      element = e;
+    }
+
+    LineWidget getWidget() {
+      return widget;
+    }
+
+    Element getElement() {
+      return element;
+    }
+  }
+
+  static class LinePaddingWidgetWrapper extends PaddingWidgetWrapper {
+    private int chunkLength;
+    private int otherLine;
+
+    LinePaddingWidgetWrapper(PaddingWidgetWrapper pair, int otherLine, int chunkLength) {
+      super(pair.widget, pair.element);
+
+      this.otherLine = otherLine;
+      this.chunkLength = chunkLength;
+    }
+
+    int getChunkLength() {
+      return chunkLength;
+    }
+
+    int getOtherLine() {
+      return otherLine;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
new file mode 100644
index 0000000..4a09615
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
@@ -0,0 +1,132 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.ClickEvent;
+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.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwtorm.client.KeyUtil;
+
+/** HTMLPanel to select among patch sets */
+class PatchSetSelectBox2 extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox2> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface BoxStyle extends CssResource {
+    String selected();
+  }
+
+  @UiField Image icon;
+  @UiField HTMLPanel linkPanel;
+  @UiField BoxStyle style;
+
+  private DiffTable table;
+  private DisplaySide side;
+  private boolean sideA;
+  private String path;
+  private Change.Id changeId;
+  private PatchSet.Id revision;
+  private PatchSet.Id idActive;
+  private PatchSetSelectBox2 other;
+
+  PatchSetSelectBox2(DiffTable table, final DisplaySide side,
+      final Change.Id changeId, final PatchSet.Id revision, String path) {
+    initWidget(uiBinder.createAndBindUi(this));
+    icon.setTitle(PatchUtil.C.addFileCommentToolTip());
+    icon.addStyleName(Gerrit.RESOURCES.css().link());
+    this.table = table;
+    this.side = side;
+    this.sideA = side == DisplaySide.A;
+    this.changeId = changeId;
+    this.revision = revision;
+    this.idActive = (sideA && revision == null) ? null : revision;
+    this.path = path;
+  }
+
+  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo.FileMeta meta) {
+    InlineHyperlink baseLink = null;
+    InlineHyperlink selectedLink = null;
+    if (sideA) {
+      baseLink = createLink(PatchUtil.C.patchBase(), null);
+      linkPanel.add(baseLink);
+    }
+    for (int i = 0; i < list.length(); i++) {
+      RevisionInfo r = list.get(i);
+      InlineHyperlink link = createLink(
+          String.valueOf(r._number()), new PatchSet.Id(changeId, r._number()));
+      linkPanel.add(link);
+      if (revision != null && r._number() == revision.get()) {
+        selectedLink = link;
+      }
+    }
+    if (selectedLink != null) {
+      selectedLink.setStyleName(style.selected());
+    } else if (sideA) {
+      baseLink.setStyleName(style.selected());
+    }
+    if (meta != null && !Patch.COMMIT_MSG.equals(path)) {
+      linkPanel.add(createDownloadLink());
+    }
+  }
+
+  static void link(PatchSetSelectBox2 a, PatchSetSelectBox2 b) {
+    a.other = b;
+    b.other = a;
+  }
+
+  private InlineHyperlink createLink(String label, PatchSet.Id id) {
+    assert other != null;
+    if (sideA) {
+      assert other.idActive != null;
+    }
+    return new InlineHyperlink(label, Dispatcher.toPatchSideBySide2(
+        sideA ? id : other.idActive,
+        sideA ? other.idActive : id,
+        path));
+  }
+
+  private Anchor createDownloadLink() {
+    PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
+    String sideURL = (idActive == null) ? "1" : "0";
+    String base = GWT.getHostPageBaseURL() + "cat/";
+    Anchor anchor = new Anchor(
+        new ImageResourceRenderer().render(Gerrit.RESOURCES.downloadIcon()),
+        base + KeyUtil.encode(id + "," + path) + "^" + sideURL);
+    anchor.setTitle(PatchUtil.C.download());
+    return anchor;
+  }
+
+  @UiHandler("icon")
+  void onIconClick(ClickEvent e) {
+    table.createOrEditFileComment(side);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
new file mode 100644
index 0000000..dca0cd5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+  <ui:with field='patchConstants'
+      type='com.google.gerrit.client.patches.PatchConstants'/>
+  <ui:style type='com.google.gerrit.client.diff.PatchSetSelectBox2.BoxStyle'>
+    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+    .table {
+      width: 100%;
+    }
+    .linkCell {
+      text-align: center;
+      font-size: 12px;
+      white-space: normal;
+      font-family: sans-serif;
+      font-weight: bold;
+    }
+    .linkCell div {
+      padding-left: 3px;
+      padding-right: 3px;
+      vertical-align: middle;
+      display: inline-block;
+    }
+    .linkCell a {
+      padding-left: 3px;
+      padding-right: 3px;
+      text-decoration: none;
+      vertical-align: middle;
+      display: inline-block;
+    }
+    .selected {
+      font-weight: bold;
+      background-color: selectionColor;
+    }
+    .hidden {
+      visibility: hidden;
+    }
+    .iconCell {
+      width: 16px;
+    }
+  </ui:style>
+  <g:HTMLPanel>
+    <table class='{style.table}'>
+      <td class='{style.iconCell}'>
+        <g:Image ui:field='icon' resource='{res.addFileComment}'/>
+      </td>
+      <td class='{style.linkCell}'>
+        <g:HTMLPanel ui:field='linkPanel'>
+          <g:Label>
+            <ui:text from='{patchConstants.patchSet}'/>
+          </g:Label>
+        </g:HTMLPanel>
+      </td>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
new file mode 100644
index 0000000..f1ea38b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -0,0 +1,207 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.CommentInput;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.Button;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import net.codemirror.lib.CodeMirror;
+
+/** An HtmlPanel for displaying a published comment */
+class PublishedBox extends CommentBox {
+  interface Binder extends UiBinder<HTMLPanel, PublishedBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  static interface Style extends CssResource {
+    String closed();
+  }
+
+  private final SideBySide2 parent;
+  private final PatchSet.Id psId;
+  private final CommentInfo comment;
+  private DraftBox replyBox;
+
+  @UiField Style style;
+  @UiField Widget header;
+  @UiField Element name;
+  @UiField Element summary;
+  @UiField Element date;
+  @UiField Element message;
+  @UiField Element buttons;
+  @UiField Button reply;
+  @UiField Button done;
+
+  @UiField(provided = true)
+  AvatarImage avatar;
+
+  PublishedBox(
+      SideBySide2 parent,
+      CodeMirror cm,
+      DisplaySide side,
+      CommentLinkProcessor clp,
+      PatchSet.Id psId,
+      CommentInfo info) {
+    super(cm, info, side);
+
+    this.parent = parent;
+    this.psId = psId;
+    this.comment = info;
+
+    if (info.author() != null) {
+      avatar = new AvatarImage(info.author());
+      avatar.setSize("", "");
+    } else {
+      avatar = new AvatarImage();
+    }
+
+    initWidget(uiBinder.createAndBindUi(this));
+    header.addDomHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        setOpen(!isOpen());
+      }
+    }, ClickEvent.getType());
+
+    name.setInnerText(authorName(info));
+    date.setInnerText(FormatUtil.shortFormatDayTime(info.updated()));
+    if (info.message() != null) {
+      String msg = info.message().trim();
+      summary.setInnerText(msg);
+      message.setInnerSafeHtml(clp.apply(
+          new SafeHtmlBuilder().append(msg).wikify()));
+    }
+  }
+
+  @Override
+  CommentInfo getCommentInfo() {
+    return comment;
+  }
+
+  @Override
+  boolean isOpen() {
+    return UIObject.isVisible(message);
+  }
+
+  void setOpen(boolean open) {
+    UIObject.setVisible(summary, !open);
+    UIObject.setVisible(message, open);
+    UIObject.setVisible(buttons, open);
+    if (open) {
+      removeStyleName(style.closed());
+    } else {
+      addStyleName(style.closed());
+    }
+    super.setOpen(open);
+  }
+
+  void registerReplyBox(DraftBox box) {
+    replyBox = box;
+    box.registerReplyToBox(this);
+  }
+
+  void unregisterReplyBox() {
+    replyBox = null;
+  }
+
+  private void openReplyBox() {
+    replyBox.setOpen(true);
+    replyBox.setEdit(true);
+  }
+
+  DraftBox addReplyBox() {
+    DraftBox box = parent.addDraftBox(parent.createReply(comment), getSide());
+    registerReplyBox(box);
+    return box;
+  }
+
+  void doReply() {
+    if (!Gerrit.isSignedIn()) {
+      Gerrit.doSignIn(parent.getToken());
+    } else if (replyBox == null) {
+      DraftBox box = addReplyBox();
+      if (!getCommentInfo().has_line()) {
+        parent.addFileCommentBox(box);
+      }
+    } else {
+      openReplyBox();
+    }
+  }
+
+  @UiHandler("reply")
+  void onReply(ClickEvent e) {
+    e.stopPropagation();
+    doReply();
+  }
+
+  @UiHandler("done")
+  void onReplyDone(ClickEvent e) {
+    e.stopPropagation();
+    if (!Gerrit.isSignedIn()) {
+      Gerrit.doSignIn(parent.getToken());
+    } else if (replyBox == null) {
+      done.setEnabled(false);
+      CommentInput input = CommentInput.create(parent.createReply(comment));
+      input.setMessage(PatchUtil.C.cannedReplyDone());
+      CommentApi.createDraft(psId, input,
+          new GerritCallback<CommentInfo>() {
+            @Override
+            public void onSuccess(CommentInfo result) {
+              done.setEnabled(true);
+              setOpen(false);
+              DraftBox box = parent.addDraftBox(result, getSide());
+              registerReplyBox(box);
+              if (!getCommentInfo().has_line()) {
+                parent.addFileCommentBox(box);
+              }
+            }
+          });
+    } else {
+      openReplyBox();
+      setOpen(false);
+    }
+  }
+
+  private static String authorName(CommentInfo info) {
+    if (info.author() != null) {
+      if (info.author().name() != null) {
+        return info.author().name();
+      }
+      return Gerrit.getConfig().getAnonymousCowardName();
+    }
+    return Util.C.messageNoAuthor();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
new file mode 100644
index 0000000..f3beda1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gerrit.client'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
+  <ui:style type='com.google.gerrit.client.diff.PublishedBox.Style'>
+    .avatar {
+      position: absolute;
+      width: 26px;
+      height: 26px;
+    }
+    .closed .avatar {
+      position: absolute;
+      width: 16px;
+      height: 16px;
+    }
+
+    .name {
+      white-space: nowrap;
+      font-weight: bold;
+    }
+    .closed .name {
+      width: 120px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      font-weight: normal;
+    }
+  </ui:style>
+
+  <g:HTMLPanel
+      styleName='{res.style.commentBox}'
+      addStyleNames='{style.closed}'>
+    <c:AvatarImage ui:field='avatar' styleName='{style.avatar}'/>
+    <div class='{res.style.contents}'>
+      <g:HTMLPanel ui:field='header' styleName='{res.style.header}'>
+        <div ui:field='name' class='{style.name}'/>
+        <div ui:field='summary' class='{res.style.summary}'/>
+        <div ui:field='date' class='{res.style.date}'/>
+      </g:HTMLPanel>
+      <div ui:field='message' aria-hidden='true' style='display: NONE'/>
+      <div ui:field='buttons' aria-hidden='true' style='display: NONE'>
+        <g:Button ui:field='reply' styleName=''
+            title='Reply to this comment'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Reply</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='done' styleName=''
+            title='Reply "Done" to this comment'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Done</ui:msg></div>
+        </g:Button>
+      </div>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
new file mode 100644
index 0000000..b7840c2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+
+/** Resources used by diff. */
+interface Resources extends ClientBundle {
+  static final Resources I = GWT.create(Resources.class);
+
+  @Source("CommentBoxUi.css") Style style();
+
+  interface Style extends CssResource {
+    String commentBox();
+    String contents();
+    String header();
+    String summary();
+    String date();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
new file mode 100644
index 0000000..f5ae149
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+import com.google.gwt.user.client.Timer;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.Viewport;
+import net.codemirror.lib.ScrollInfo;
+
+class ScrollSynchronizer {
+  private DiffTable diffTable;
+  private LineMapper mapper;
+  private ScrollCallback active;
+
+  void init(DiffTable diffTable,
+      CodeMirror cmA, CodeMirror cmB,
+      LineMapper mapper) {
+    this.diffTable = diffTable;
+    this.mapper = mapper;
+
+    cmA.on("scroll", new ScrollCallback(cmA, cmB, DisplaySide.A));
+    cmB.on("scroll", new ScrollCallback(cmB, cmA, DisplaySide.B));
+  }
+
+  private void updateScreenHeader(ScrollInfo si) {
+    if (si.getTop() == 0 && !Gerrit.isHeaderVisible()) {
+      Gerrit.setHeaderVisible(true);
+      diffTable.updateFileCommentVisibility(false);
+    } else if (si.getTop() > 0.5 * si.getClientHeight()
+        && Gerrit.isHeaderVisible()) {
+      Gerrit.setHeaderVisible(false);
+      diffTable.updateFileCommentVisibility(true);
+    }
+  }
+
+  class ScrollCallback implements Runnable {
+    private final CodeMirror src;
+    private final CodeMirror dst;
+    private final DisplaySide srcSide;
+    private final Timer fixup;
+    private int state;
+
+    ScrollCallback(CodeMirror src, CodeMirror dst, DisplaySide srcSide) {
+      this.src = src;
+      this.dst = dst;
+      this.srcSide = srcSide;
+      this.fixup = new Timer() {
+        @Override
+        public void run() {
+          if (active == ScrollCallback.this) {
+            fixup();
+          }
+        }
+      };
+    }
+
+    @Override
+    public void run() {
+      if (active == null) {
+        active = this;
+        fixup.scheduleRepeating(20);
+      }
+      if (active == this) {
+        ScrollInfo si = src.getScrollInfo();
+        updateScreenHeader(si);
+        dst.scrollTo(si.getLeft(), si.getTop());
+        state = 0;
+      }
+    }
+
+    private void fixup() {
+      switch (state) {
+        case 0:
+          state = 1;
+          break;
+        case 1:
+          state = 2;
+          return;
+        case 2:
+          active = null;
+          fixup.cancel();
+          return;
+      }
+
+      // Since CM doesn't always take the height of line widgets into
+      // account when calculating scrollInfo when scrolling too fast (e.g.
+      // throw scrolling), simply setting scrollTop to be the same doesn't
+      // guarantee alignment.
+      //
+      // Iterate over the viewport to find the first line that isn't part of
+      // an insertion or deletion gap, for which isAligned() will be true.
+      // We then manually examine if the lines that should be aligned are at
+      // the same height. If not, perform additional scrolling.
+      Viewport fromTo = src.getViewport();
+      for (int line = fromTo.getFrom(); line <= fromTo.getTo(); line++) {
+        LineOnOtherInfo info = mapper.lineOnOther(srcSide, line);
+        if (info.isAligned()) {
+          double sy = src.heightAtLine(line);
+          double dy = dst.heightAtLine(info.getLine());
+          if (Math.abs(dy - sy) >= 1) {
+            dst.scrollToY(dst.getScrollInfo().getTop() + (dy - sy));
+          }
+          break;
+        }
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
new file mode 100644
index 0000000..393ff73
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
@@ -0,0 +1,1298 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.change.ChangeScreen2;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.diff.DiffInfo.Region;
+import com.google.gerrit.client.diff.DiffInfo.Span;
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+import com.google.gerrit.client.diff.PaddingManager.LinePaddingWidgetWrapper;
+import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.patches.SkippedLine;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.globalkey.client.ShowHelpCommand;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.GutterClickHandler;
+import net.codemirror.lib.CodeMirror.LineClassWhere;
+import net.codemirror.lib.CodeMirror.LineHandle;
+import net.codemirror.lib.CodeMirror.RenderLineHandler;
+import net.codemirror.lib.CodeMirror.Viewport;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.KeyMap;
+import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.ModeInjector;
+import net.codemirror.lib.TextMarker.FromTo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SideBySide2 extends Screen {
+  interface Binder extends UiBinder<FlowPanel, SideBySide2> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private static final JsArrayString EMPTY =
+      JavaScriptObject.createArray().cast();
+
+  @UiField(provided = true)
+  Header header;
+
+  @UiField(provided = true)
+  DiffTable diffTable;
+
+  private final Change.Id changeId;
+  private final PatchSet.Id base;
+  private final PatchSet.Id revision;
+  private final String path;
+  private AccountDiffPreference pref;
+
+  private CodeMirror cmA;
+  private CodeMirror cmB;
+  private ScrollSynchronizer scrollingGlue;
+  private HandlerRegistration resizeHandler;
+  private JsArray<CommentInfo> publishedBase;
+  private JsArray<CommentInfo> publishedRevision;
+  private JsArray<CommentInfo> draftsBase;
+  private JsArray<CommentInfo> draftsRevision;
+  private DiffInfo diff;
+  private LineMapper mapper;
+  private CommentLinkProcessor commentLinkProcessor;
+  private Map<String, PublishedBox> publishedMap;
+  private Map<LineHandle, CommentBox> lineActiveBoxMap;
+  private Map<LineHandle, List<PublishedBox>> linePublishedBoxesMap;
+  private Map<LineHandle, PaddingManager> linePaddingManagerMap;
+  private Map<LineHandle, LinePaddingWidgetWrapper> linePaddingOnOtherSideMap;
+  private List<DiffChunkInfo> diffChunks;
+  private List<SkippedLine> skips;
+  private int context;
+
+  private KeyCommandSet keysNavigation;
+  private KeyCommandSet keysAction;
+  private KeyCommandSet keysComment;
+  private KeyCommandSet keysOpenByEnter;
+  private List<HandlerRegistration> handlers;
+
+  public SideBySide2(
+      PatchSet.Id base,
+      PatchSet.Id revision,
+      String path) {
+    this.base = base;
+    this.revision = revision;
+    this.changeId = revision.getParentKey();
+    this.path = path;
+
+    pref = Gerrit.getAccountDiffPreference();
+    if (pref == null) {
+      pref = AccountDiffPreference.createDefault(null);
+    }
+    context = pref.getContext();
+
+    handlers = new ArrayList<HandlerRegistration>(6);
+    // TODO: Re-implement necessary GlobalKey bindings.
+    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
+    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
+    header = new Header(keysNavigation, base, revision, path);
+    diffTable = new DiffTable(this, base, revision, path);
+    add(uiBinder.createAndBindUi(this));
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setHeaderVisible(false);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    CallbackGroup cmGroup = new CallbackGroup();
+    CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void> emptyCallback()));
+    final CallbackGroup group = new CallbackGroup();
+    final AsyncCallback<Void> modeInjectorCb =
+        group.add(CallbackGroup.<Void> emptyCallback());
+
+    DiffApi.diff(revision, path)
+      .base(base)
+      .wholeFile()
+      .intraline(pref.isIntralineDifference())
+      .ignoreWhitespace(pref.getIgnoreWhitespace())
+      .get(cmGroup.addFinal(new GerritCallback<DiffInfo>() {
+        @Override
+        public void onSuccess(DiffInfo diffInfo) {
+          diff = diffInfo;
+          new ModeInjector()
+            .add(getContentType(diff.meta_a()))
+            .add(getContentType(diff.meta_b()))
+            .inject(modeInjectorCb);
+        }
+      }));
+
+    if (base != null) {
+      CommentApi.comments(base, group.add(getCommentCallback(DisplaySide.A, false)));
+    }
+    CommentApi.comments(revision, group.add(getCommentCallback(DisplaySide.B, false)));
+
+    if (Gerrit.isSignedIn()) {
+      if (base != null) {
+        CommentApi.drafts(base, group.add(getCommentCallback(DisplaySide.A, true)));
+      }
+      CommentApi.drafts(revision, group.add(getCommentCallback(DisplaySide.B, true)));
+    }
+
+    RestApi call = ChangeApi.detail(changeId.get());
+    ChangeList.addOptions(call, EnumSet.of(
+        ListChangesOption.ALL_REVISIONS));
+    call.get(group.add(new GerritCallback<ChangeInfo>() {
+      @Override
+      public void onSuccess(ChangeInfo info) {
+        info.revisions().copyKeysIntoChildren("name");
+        JsArray<RevisionInfo> list = info.revisions().values();
+        RevisionInfo.sortRevisionInfoByNumber(list);
+        diffTable.setUpPatchSetNav(list, diff);
+      }}));
+
+    ConfigInfoCache.get(changeId, group.addFinal(
+        new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide2.this) {
+          @Override
+          protected void preDisplay(ConfigInfoCache.Entry result) {
+            commentLinkProcessor = result.getCommentLinkProcessor();
+            setTheme(result.getTheme());
+
+            DiffInfo diffInfo = diff;
+            diff = null;
+            display(diffInfo);
+          }
+        }));
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    resizeCodeMirror();
+    Window.enableScrolling(false);
+
+    cmA.setOption("viewportMargin", 10);
+    cmB.setOption("viewportMargin", 10);
+    cmB.setCursor(LineCharacter.create(0));
+    cmB.focus();
+
+    prefetchNextFile();
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+
+    removeKeyHandlerRegs();
+    if (resizeHandler != null) {
+      resizeHandler.removeHandler();
+      resizeHandler = null;
+    }
+    if (cmA != null) {
+      cmA.getWrapperElement().removeFromParent();
+    }
+    if (cmB != null) {
+      cmB.getWrapperElement().removeFromParent();
+    }
+
+    Window.enableScrolling(true);
+    Gerrit.setHeaderVisible(true);
+  }
+
+  private void removeKeyHandlerRegs() {
+    for (HandlerRegistration h : handlers) {
+      h.removeHandler();
+    }
+    handlers.clear();
+  }
+
+  private void registerCmEvents(final CodeMirror cm) {
+    cm.on("cursorActivity", updateActiveLine(cm));
+    cm.on("gutterClick", onGutterClick(cm));
+    cm.on("renderLine", resizeLinePadding(getSideFromCm(cm)));
+    cm.on("viewportChange", adjustGutters(cm));
+    cm.on("focus", new Runnable() {
+      @Override
+      public void run() {
+        updateActiveLine(cm).run();
+      }
+    });
+    cm.addKeyMap(KeyMap.create()
+        .on("'a'", upToChange(true))
+        .on("'u'", upToChange(false))
+        .on("'r'", toggleReviewed())
+        .on("'o'", toggleOpenBox(cm))
+        .on("Enter", toggleOpenBox(cm))
+        .on("'c'", insertNewDraft(cm))
+        .on("Alt-U", new Runnable() {
+          public void run() {
+            cm.getInputField().blur();
+            clearActiveLine(cm);
+            clearActiveLine(otherCm(cm));
+          }
+        })
+        .on("[", new Runnable() {
+          @Override
+          public void run() {
+            (header.hasPrev() ? header.prev : header.up).go();
+          }
+        })
+        .on("]", new Runnable() {
+          @Override
+          public void run() {
+            (header.hasNext() ? header.next : header.up).go();
+          }
+        })
+        .on("Shift-/", new Runnable() {
+          @Override
+          public void run() {
+            new ShowHelpCommand().onKeyPress(null);
+          }
+        })
+        .on("Ctrl-F", new Runnable() {
+          @Override
+          public void run() {
+            CodeMirror.handleVimKey(cm, "/");
+          }
+        })
+        .on("Space", new Runnable() {
+          @Override
+          public void run() {
+            CodeMirror.handleVimKey(cm, "<PageDown>");
+          }
+        })
+        .on("Ctrl-A", new Runnable() {
+          @Override
+          public void run() {
+            cm.execCommand("selectAll");
+          }
+        })
+        .on("N", maybeNextVimSearch(cm))
+        .on("P", diffChunkNav(cm, true))
+        .on("Shift-O", openClosePublished(cm))
+        .on("Shift-Left", flipCursorSide(cm, true))
+        .on("Shift-Right", flipCursorSide(cm, false)));
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+
+    keysNavigation.add(new UpToChangeCommand2(revision, 0, 'u'));
+    keysNavigation.add(new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()));
+    keysNavigation.add(new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
+    keysNavigation.add(new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext2()));
+    keysNavigation.add(new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev2()));
+
+    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+    keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
+    keysAction.add(new NoOpKeyCommand(
+        KeyCommand.M_SHIFT, 'o', PatchUtil.C.expandAllCommentsOnCurrentLine()));
+    keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        toggleReviewed().run();
+      }
+    });
+    keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        upToChange(true).run();
+      }
+    });
+
+    keysOpenByEnter = new KeyCommandSet(Gerrit.C.sectionNavigation());
+    keysOpenByEnter.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
+        PatchUtil.C.expandComment()));
+
+    if (Gerrit.isSignedIn()) {
+      keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
+      keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
+      keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's',
+          PatchUtil.C.commentSaveDraft()));
+      keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE,
+          PatchUtil.C.commentCancelEdit()));
+    } else {
+      keysComment = null;
+    }
+    removeKeyHandlerRegs();
+    handlers.add(GlobalKey.add(this, keysNavigation));
+    handlers.add(GlobalKey.add(this, keysAction));
+    handlers.add(GlobalKey.add(this, keysOpenByEnter));
+    if (keysComment != null) {
+      handlers.add(GlobalKey.add(this, keysComment));
+    }
+  }
+
+  private GerritCallback<NativeMap<JsArray<CommentInfo>>> getCommentCallback(
+      final DisplaySide side, final boolean toDrafts) {
+    return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
+      @Override
+      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+        JsArray<CommentInfo> in = result.get(path);
+        if (in != null) {
+          if (toDrafts) {
+            if (side == DisplaySide.A) {
+              draftsBase = in;
+            } else {
+              draftsRevision = in;
+            }
+          } else {
+            if (side == DisplaySide.A) {
+              publishedBase = in;
+            } else {
+              publishedRevision = in;
+            }
+          }
+        }
+      }
+    };
+  }
+
+  private void display(DiffInfo diffInfo) {
+    cmA = displaySide(diffInfo.meta_a(), diffInfo.text_a(), diffTable.cmA);
+    cmB = displaySide(diffInfo.meta_b(), diffInfo.text_b(), diffTable.cmB);
+
+    skips = new ArrayList<SkippedLine>();
+    linePaddingOnOtherSideMap = new HashMap<LineHandle, LinePaddingWidgetWrapper>();
+    diffChunks = new ArrayList<DiffChunkInfo>();
+    render(diffInfo);
+    lineActiveBoxMap = new HashMap<LineHandle, CommentBox>();
+    linePublishedBoxesMap = new HashMap<LineHandle, List<PublishedBox>>();
+    linePaddingManagerMap = new HashMap<LineHandle, PaddingManager>();
+    if (publishedBase != null || publishedRevision != null) {
+      publishedMap = new HashMap<String, PublishedBox>();
+    }
+    if (publishedBase != null) {
+      renderPublished(publishedBase);
+    }
+    if (publishedRevision != null) {
+      renderPublished(publishedRevision);
+    }
+    if (draftsBase != null) {
+      renderDrafts(draftsBase);
+    }
+    if (draftsRevision != null) {
+      renderDrafts(draftsRevision);
+    }
+    renderSkips();
+    registerCmEvents(cmA);
+    registerCmEvents(cmB);
+
+    scrollingGlue = GWT.create(ScrollSynchronizer.class);
+    scrollingGlue.init(diffTable, cmA, cmB, mapper);
+
+    resizeHandler = Window.addResizeHandler(new ResizeHandler() {
+      @Override
+      public void onResize(ResizeEvent event) {
+        resizeCodeMirror();
+      }
+    });
+    if (pref.isShowTabs()) {
+      diffTable.addStyleName(DiffTable.style.showtabs());
+    }
+  }
+
+  private CodeMirror displaySide(DiffInfo.FileMeta meta, String contents,
+      Element ele) {
+    if (meta == null) {
+      contents = "";
+    }
+    Configuration cfg = Configuration.create()
+      .set("readOnly", true)
+      .set("cursorBlinkRate", 0)
+      .set("cursorHeight", 0.85)
+      .set("lineNumbers", true)
+      .set("tabSize", pref.getTabSize())
+      .set("mode", getContentType(meta))
+      .set("lineWrapping", false)
+      .set("styleSelectedText", true)
+      .set("showTrailingSpace", pref.isShowWhitespaceErrors())
+      .set("keyMap", "vim_ro")
+      .set("value", contents)
+      /**
+       * Without this, CM won't put line widgets too far down in the right spot,
+       * and padding widgets will be informed of wrong offset height. Reset to
+       * 10 (default) after initial rendering.
+       */
+      .setInfinity("viewportMargin");
+    int h = Gerrit.getHeaderFooterHeight() + 18 /* reviewed estimate */;
+    CodeMirror cm = CodeMirror.create(ele, cfg);
+    cm.setHeight(Window.getClientHeight() - h);
+    return cm;
+  }
+
+  private void render(DiffInfo diff) {
+    JsArray<Region> regions = diff.content();
+    if (!(regions.length() == 0 ||
+        regions.length() == 1 && regions.get(0).ab() != null)) {
+      header.removeNoDiff();
+    }
+
+    String diffColor = diff.meta_a() == null || diff.meta_b() == null
+        ? DiffTable.style.intralineBg()
+        : DiffTable.style.diff();
+    mapper = new LineMapper();
+    for (int i = 0; i < regions.length(); i++) {
+      Region current = regions.get(i);
+      int origLineA = mapper.getLineA();
+      int origLineB = mapper.getLineB();
+      if (current.ab() != null) { // Common
+        int length = current.ab().length();
+        mapper.appendCommon(length);
+        if (i == 0 && length > context + 1) {
+          skips.add(new SkippedLine(0, 0, length - context));
+        } else if (i == regions.length() - 1 && length > context + 1) {
+          skips.add(new SkippedLine(origLineA + context, origLineB + context,
+              length - context));
+        } else if (length > 2 * context + 1) {
+          skips.add(new SkippedLine(origLineA + context, origLineB + context,
+              length - 2 * context));
+        }
+      } else { // Insert, Delete or Edit
+        JsArrayString currentA = current.a() == null ? EMPTY : current.a();
+        JsArrayString currentB = current.b() == null ? EMPTY : current.b();
+        int aLength = currentA.length();
+        int bLength = currentB.length();
+        String color = currentA == EMPTY || currentB == EMPTY
+            ? diffColor
+            : DiffTable.style.intralineBg();
+        colorLines(cmA, color, origLineA, aLength);
+        colorLines(cmB, color, origLineB, bLength);
+        int commonCnt = Math.min(aLength, bLength);
+        mapper.appendCommon(commonCnt);
+        if (aLength < bLength) { // Edit with insertion
+          int insertCnt = bLength - aLength;
+          mapper.appendInsert(insertCnt);
+        } else if (aLength > bLength) { // Edit with deletion
+          int deleteCnt = aLength - bLength;
+          mapper.appendDelete(deleteCnt);
+        }
+        int chunkEndA = mapper.getLineA() - 1;
+        int chunkEndB = mapper.getLineB() - 1;
+        if (aLength > 0) {
+          addDiffChunkAndPadding(cmB, chunkEndB, chunkEndA, aLength, bLength > 0);
+        }
+        if (bLength > 0) {
+          addDiffChunkAndPadding(cmA, chunkEndA, chunkEndB, bLength, aLength > 0);
+        }
+        markEdit(cmA, currentA, current.edit_a(), origLineA);
+        markEdit(cmB, currentB, current.edit_b(), origLineB);
+        if (aLength == 0) {
+          diffTable.sidePanel.addGutter(cmB, origLineB, SidePanel.GutterType.INSERT);
+        } else if (bLength == 0) {
+          diffTable.sidePanel.addGutter(cmA, origLineA, SidePanel.GutterType.DELETE);
+        } else {
+          diffTable.sidePanel.addGutter(cmB, origLineB, SidePanel.GutterType.EDIT);
+        }
+      }
+    }
+  }
+
+  private DraftBox addNewDraft(CodeMirror cm, int line, FromTo fromTo) {
+    DisplaySide side = getSideFromCm(cm);
+    return addDraftBox(CommentInfo.createRange(
+        path,
+        getStoredSideFromDisplaySide(side),
+        line + 1,
+        null,
+        null,
+        CommentRange.create(fromTo)), side);
+  }
+
+  CommentInfo createReply(CommentInfo replyTo) {
+    if (!replyTo.has_line() && replyTo.range() == null) {
+      return CommentInfo.createFile(path, replyTo.side(), replyTo.id(), null);
+    } else {
+      return CommentInfo.createRange(path, replyTo.side(), replyTo.line(),
+          replyTo.id(), null, replyTo.range());
+    }
+  }
+
+  DraftBox addDraftBox(CommentInfo info, DisplaySide side) {
+    CodeMirror cm = getCmFromSide(side);
+    final DraftBox box = new DraftBox(this, cm, side, commentLinkProcessor,
+        getPatchSetIdFromSide(side), info);
+    if (info.id() == null) {
+      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+        @Override
+        public void execute() {
+          box.setOpen(true);
+          box.setEdit(true);
+        }
+      });
+    }
+    if (!info.has_line()) {
+      return box;
+    }
+    addCommentBox(info, box);
+    LineHandle handle = cm.getLineHandle(info.line() - 1);
+    lineActiveBoxMap.put(handle, box);
+    return box;
+  }
+
+  CommentBox addCommentBox(CommentInfo info, CommentBox box) {
+    diffTable.add(box);
+    DisplaySide side = box.getSide();
+    CodeMirror cm = getCmFromSide(side);
+    CodeMirror other = otherCm(cm);
+    int line = info.line() - 1; // CommentInfo is 1-based, but CM is 0-based
+    LineHandle handle = cm.getLineHandle(line);
+    PaddingManager manager;
+    if (linePaddingManagerMap.containsKey(handle)) {
+      manager = linePaddingManagerMap.get(handle);
+    } else {
+      // Estimated height at 28px, fixed by deferring after display
+      manager = new PaddingManager(addPaddingWidget(cm, line, 0, Unit.PX, 0));
+      linePaddingManagerMap.put(handle, manager);
+    }
+    int lineToPad = mapper.lineOnOther(side, line).getLine();
+    LineHandle otherHandle = other.getLineHandle(lineToPad);
+    DiffChunkInfo myChunk = getDiffChunk(side, line);
+    DiffChunkInfo otherChunk = getDiffChunk(getSideFromCm(other), lineToPad);
+    PaddingManager otherManager;
+    if (linePaddingManagerMap.containsKey(otherHandle)) {
+      otherManager = linePaddingManagerMap.get(otherHandle);
+    } else {
+      otherManager =
+          new PaddingManager(addPaddingWidget(other, lineToPad, 0, Unit.PX, 0));
+      linePaddingManagerMap.put(otherHandle, otherManager);
+    }
+    if ((myChunk == null && otherChunk == null) || (myChunk != null && otherChunk != null)) {
+      PaddingManager.link(manager, otherManager);
+    }
+    int index = manager.getCurrentCount();
+    manager.insert(box, index);
+    Configuration config = Configuration.create()
+      .set("coverGutter", true)
+      .set("insertAt", index)
+      .set("noHScroll", true);
+    LineWidget boxWidget = cm.addLineWidget(line, box.getElement(), config);
+    box.setPaddingManager(manager);
+    box.setSelfWidgetWrapper(new PaddingWidgetWrapper(boxWidget, box.getElement()));
+    box.setParent(this);
+    if (otherChunk == null) {
+      box.setDiffChunkInfo(myChunk);
+    }
+    box.setGutterWrapper(diffTable.sidePanel.addGutter(cm, info.line() - 1,
+        box instanceof DraftBox ?
+            SidePanel.GutterType.DRAFT
+          : SidePanel.GutterType.COMMENT));
+    return box;
+  }
+
+  void removeDraft(DraftBox box, int line) {
+    LineHandle handle = getCmFromSide(box.getSide()).getLineHandle(line);
+    lineActiveBoxMap.remove(handle);
+    if (linePublishedBoxesMap.containsKey(handle)) {
+      List<PublishedBox> list = linePublishedBoxesMap.get(handle);
+      lineActiveBoxMap.put(handle, list.get(list.size() - 1));
+    }
+  }
+
+  void addFileCommentBox(CommentBox box) {
+    diffTable.addFileCommentBox(box);
+  }
+
+  void removeFileCommentBox(DraftBox box) {
+    diffTable.onRemoveDraftBox(box);
+  }
+
+  private List<CommentInfo> sortComment(JsArray<CommentInfo> unsorted) {
+    List<CommentInfo> sorted = new ArrayList<CommentInfo>();
+    for (int i = 0; i < unsorted.length(); i++) {
+      sorted.add(unsorted.get(i));
+    }
+    Collections.sort(sorted, new Comparator<CommentInfo>() {
+      @Override
+      public int compare(CommentInfo o1, CommentInfo o2) {
+        return o1.updated().compareTo(o2.updated());
+      }
+    });
+    return sorted;
+  }
+
+  private void renderPublished(JsArray<CommentInfo> published) {
+    List<CommentInfo> sorted = sortComment(published);
+    for (CommentInfo info : sorted) {
+      DisplaySide side;
+      if (info.side() == Side.PARENT) {
+        if (base != null) {
+          continue;
+        }
+        side = DisplaySide.A;
+      } else {
+        side = published == publishedBase ? DisplaySide.A : DisplaySide.B;
+      }
+      CodeMirror cm = getCmFromSide(side);
+      PublishedBox box = new PublishedBox(this, cm, side, commentLinkProcessor,
+          getPatchSetIdFromSide(side), info);
+      publishedMap.put(info.id(), box);
+      if (!info.has_line()) {
+        diffTable.addFileCommentBox(box);
+        continue;
+      }
+      int line = info.line() - 1;
+      LineHandle handle = cm.getLineHandle(line);
+      if (linePublishedBoxesMap.containsKey(handle)) {
+        linePublishedBoxesMap.get(handle).add(box);
+      } else {
+        List<PublishedBox> list = new ArrayList<PublishedBox>();
+        list.add(box);
+        linePublishedBoxesMap.put(handle, list);
+      }
+      lineActiveBoxMap.put(handle, box);
+      addCommentBox(info, box);
+    }
+  }
+
+  private void renderDrafts(JsArray<CommentInfo> drafts) {
+    List<CommentInfo> sorted = sortComment(drafts);
+    for (CommentInfo info : sorted) {
+      DisplaySide side;
+      if (info.side() == Side.PARENT) {
+        if (base != null) {
+          continue;
+        }
+        side = DisplaySide.A;
+      } else {
+        side = drafts == draftsBase ? DisplaySide.A : DisplaySide.B;
+      }
+      DraftBox box = new DraftBox(
+          this, getCmFromSide(side), side, commentLinkProcessor,
+          getPatchSetIdFromSide(side), info);
+      if (publishedBase != null || publishedRevision != null) {
+        PublishedBox replyToBox = publishedMap.get(info.in_reply_to());
+        if (replyToBox != null) {
+          replyToBox.registerReplyBox(box);
+        }
+      }
+      if (!info.has_line()) {
+        diffTable.addFileCommentBox(box);
+        continue;
+      }
+      lineActiveBoxMap.put(
+          getCmFromSide(side).getLineHandle(info.line() - 1), box);
+      addCommentBox(info, box);
+    }
+  }
+
+  private void renderSkips() {
+    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+      return;
+    }
+
+    /**
+     * TODO: This is not optimal, but shouldn't bee too costly in most cases.
+     * Maybe rewrite after done keeping track of diff chunk positions.
+     */
+    for (CommentBox box : lineActiveBoxMap.values()) {
+      List<SkippedLine> temp = new ArrayList<SkippedLine>();
+      for (SkippedLine skip : skips) {
+        CommentInfo info = box.getCommentInfo();
+        int startLine = box.getSide() == DisplaySide.A
+            ? skip.getStartA()
+            : skip.getStartB();
+        int boxLine = info.line();
+        int deltaBefore = boxLine - startLine;
+        int deltaAfter = startLine + skip.getSize() - boxLine;
+        if (deltaBefore < -context || deltaAfter < -context) {
+          temp.add(skip); // Size guaranteed to be greater than 1
+        } else if (deltaBefore > context && deltaAfter > context) {
+          SkippedLine before = new SkippedLine(
+              skip.getStartA(), skip.getStartB(),
+              skip.getSize() - deltaAfter - context);
+          skip.incrementStart(deltaBefore + context);
+          checkAndAddSkip(temp, before);
+          checkAndAddSkip(temp, skip);
+        } else if (deltaAfter > context) {
+          skip.incrementStart(deltaBefore + context);
+          checkAndAddSkip(temp, skip);
+        } else if (deltaBefore > context) {
+          skip.reduceSize(deltaAfter + context);
+          checkAndAddSkip(temp, skip);
+        }
+      }
+      if (temp.isEmpty()) {
+        return;
+      }
+      skips = temp;
+    }
+    for (SkippedLine skip : skips) {
+      SkipBar barA = renderSkipHelper(cmA, skip);
+      SkipBar barB = renderSkipHelper(cmB, skip);
+      SkipBar.link(barA, barB);
+    }
+  }
+
+  private void checkAndAddSkip(List<SkippedLine> list, SkippedLine toAdd) {
+    if (toAdd.getSize() > 1) {
+      list.add(toAdd);
+    }
+  }
+
+  private SkipBar renderSkipHelper(CodeMirror cm, SkippedLine skip) {
+    int size = skip.getSize();
+    int markStart = cm == cmA ? skip.getStartA() : skip.getStartB();
+    int markEnd = markStart + size;
+    SkipBar bar = new SkipBar(cm);
+    diffTable.add(bar);
+    Configuration markerConfig = Configuration.create()
+        .set("collapsed", true)
+        .set("inclusiveLeft", true)
+        .set("inclusiveRight", true);
+    Configuration lineWidgetConfig = Configuration.create()
+        .set("coverGutter", true)
+        .set("noHScroll", true);
+    if (markStart == 0) {
+      bar.setWidget(cm.addLineWidget(
+          markEnd + 1, bar.getElement(), lineWidgetConfig.set("above", true)));
+    } else {
+      bar.setWidget(cm.addLineWidget(
+          markStart - 1, bar.getElement(), lineWidgetConfig));
+    }
+    bar.setMarker(cm.markText(CodeMirror.pos(markStart, 0),
+        CodeMirror.pos(markEnd), markerConfig), size);
+    return bar;
+  }
+
+  private CodeMirror otherCm(CodeMirror me) {
+    return me == cmA ? cmB : cmA;
+  }
+
+  private PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
+    return side == DisplaySide.A && base != null ? base : revision;
+  }
+
+  private CodeMirror getCmFromSide(DisplaySide side) {
+    return side == DisplaySide.A ? cmA : cmB;
+  }
+
+  private DisplaySide getSideFromCm(CodeMirror cm) {
+    return cm == cmA ? DisplaySide.A : DisplaySide.B;
+  }
+
+  Side getStoredSideFromDisplaySide(DisplaySide side) {
+    return side == DisplaySide.A && base == null ? Side.PARENT : Side.REVISION;
+  }
+
+  private void markEdit(CodeMirror cm, JsArrayString lines,
+      JsArray<Span> edits, int startLine) {
+    if (edits == null) {
+      return;
+    }
+    EditIterator iter = new EditIterator(lines, startLine);
+    Configuration intralineBgOpt = Configuration.create()
+        .set("className", DiffTable.style.intralineBg())
+        .set("readOnly", true);
+    Configuration diffOpt = Configuration.create()
+        .set("className", DiffTable.style.diff())
+        .set("readOnly", true);
+    LineCharacter last = CodeMirror.pos(0, 0);
+    for (int i = 0; i < edits.length(); i++) {
+      Span span = edits.get(i);
+      LineCharacter from = iter.advance(span.skip());
+      LineCharacter to = iter.advance(span.mark());
+      int fromLine = from.getLine();
+      if (last.getLine() == fromLine) {
+        cm.markText(last, from, intralineBgOpt);
+      } else {
+        cm.markText(CodeMirror.pos(fromLine, 0), from, intralineBgOpt);
+      }
+      cm.markText(from, to, diffOpt);
+      last = to;
+      for (int line = fromLine; line < to.getLine(); line++) {
+        cm.addLineClass(line, LineClassWhere.BACKGROUND,
+            DiffTable.style.diff());
+      }
+    }
+  }
+
+  private void colorLines(CodeMirror cm, String color, int line, int cnt) {
+    for (int i = 0; i < cnt; i++) {
+      cm.addLineClass(line + i, LineClassWhere.WRAP, color);
+    }
+  }
+
+  private void addDiffChunkAndPadding(CodeMirror cmToPad, int lineToPad,
+      int lineOnOther, int chunkSize, boolean edit) {
+    CodeMirror otherCm = otherCm(cmToPad);
+    linePaddingOnOtherSideMap.put(otherCm.getLineHandle(lineOnOther),
+        new LinePaddingWidgetWrapper(addPaddingWidget(cmToPad,
+            lineToPad, 0, Unit.EM, null), lineToPad, chunkSize));
+    diffChunks.add(new DiffChunkInfo(getSideFromCm(otherCm),
+        lineOnOther - chunkSize + 1, lineOnOther, edit));
+  }
+
+  private PaddingWidgetWrapper addPaddingWidget(CodeMirror cm,
+      int line, double height, Unit unit, Integer index) {
+    Element div = DOM.createDiv();
+    div.getStyle().setHeight(height, unit);
+    Configuration config = Configuration.create()
+        .set("coverGutter", true)
+        .set("above", line == -1)
+        .set("noHScroll", true);
+    if (index != null) {
+      config = config.set("insertAt", index);
+    }
+    LineWidget widget = cm.addLineWidget(line == -1 ? 0 : line, div, config);
+    return new PaddingWidgetWrapper(widget, div);
+  }
+
+  private void clearActiveLine(CodeMirror cm) {
+    if (cm.hasActiveLine()) {
+      LineHandle activeLine = cm.getActiveLine();
+      cm.removeLineClass(activeLine,
+          LineClassWhere.WRAP, DiffTable.style.activeLine());
+      cm.setActiveLine(null);
+    }
+  }
+
+  private Runnable adjustGutters(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        Viewport fromTo = cm.getViewport();
+        int size = fromTo.getTo() - fromTo.getFrom() + 1;
+        if (cm.getOldViewportSize() == size) {
+          return;
+        }
+        cm.setOldViewportSize(size);
+        diffTable.sidePanel.adjustGutters(cmB);
+      }
+    };
+  }
+
+  private Runnable updateActiveLine(final CodeMirror cm) {
+    final CodeMirror other = otherCm(cm);
+    return new Runnable() {
+      public void run() {
+        /**
+         * The rendering of active lines has to be deferred. Reflow
+         * caused by adding and removing styles chokes Firefox when arrow
+         * key (or j/k) is held down. Performance on Chrome is fine
+         * without the deferral.
+         */
+        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+          @Override
+          public void execute() {
+            LineHandle handle = cm.getLineHandleVisualStart(
+                cm.getCursor("end").getLine());
+            if (cm.hasActiveLine() && cm.getActiveLine().equals(handle)) {
+              return;
+            }
+
+            clearActiveLine(cm);
+            clearActiveLine(other);
+            cm.setActiveLine(handle);
+            cm.addLineClass(
+                handle, LineClassWhere.WRAP, DiffTable.style.activeLine());
+            LineOnOtherInfo info =
+                mapper.lineOnOther(getSideFromCm(cm), cm.getLineNumber(handle));
+            if (info.isAligned()) {
+              LineHandle oLineHandle = other.getLineHandle(info.getLine());
+              other.setActiveLine(oLineHandle);
+              other.addLineClass(oLineHandle, LineClassWhere.WRAP,
+                  DiffTable.style.activeLine());
+            }
+          }
+        });
+      }
+    };
+  }
+
+  private GutterClickHandler onGutterClick(final CodeMirror cm) {
+    return new GutterClickHandler() {
+      @Override
+      public void handle(CodeMirror instance, int line, String gutter,
+          NativeEvent clickEvent) {
+        if (!(cm.hasActiveLine() &&
+            cm.getLineNumber(cm.getActiveLine()) == line)) {
+          cm.setCursor(LineCharacter.create(line));
+        }
+        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+          @Override
+          public void execute() {
+            insertNewDraft(cm).run();
+          }
+        });
+      }
+    };
+  }
+
+  private Runnable insertNewDraft(final CodeMirror cm) {
+    if (!Gerrit.isSignedIn()) {
+      return new Runnable() {
+        @Override
+        public void run() {
+          Gerrit.doSignIn(getToken());
+        }
+     };
+    }
+    return new Runnable() {
+      public void run() {
+        LineHandle handle = cm.getActiveLine();
+        int line = cm.getLineNumber(handle);
+        CommentBox box = lineActiveBoxMap.get(handle);
+        FromTo fromTo = cm.getSelectedRange();
+        if (cm.somethingSelected()) {
+          lineActiveBoxMap.put(handle,
+              addNewDraft(cm, line, fromTo.getTo().getLine() == line ? fromTo : null));
+        } else if (box == null) {
+          lineActiveBoxMap.put(handle, addNewDraft(cm, line, null));
+        } else if (box instanceof DraftBox) {
+          ((DraftBox) box).setEdit(true);
+        } else {
+          ((PublishedBox) box).doReply();
+        }
+      }
+    };
+  }
+
+  private Runnable toggleOpenBox(final CodeMirror cm) {
+    return new Runnable() {
+      public void run() {
+        CommentBox box = lineActiveBoxMap.get(cm.getActiveLine());
+        if (box != null) {
+          box.setOpen(!box.isOpen());
+        }
+      }
+    };
+  }
+
+  private Runnable upToChange(final boolean openReplyBox) {
+    return new Runnable() {
+      public void run() {
+        String rev = String.valueOf(revision.get());
+        Gerrit.display(
+          PageLinks.toChange(changeId, rev),
+          new ChangeScreen2(changeId, rev, openReplyBox));
+      }
+    };
+  }
+
+  private Runnable openClosePublished(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.hasActiveLine()) {
+          List<PublishedBox> list =
+              linePublishedBoxesMap.get(cm.getActiveLine());
+          if (list == null) {
+            return;
+          }
+          boolean open = false;
+          for (PublishedBox box : list) {
+            if (!box.isOpen()) {
+              open = true;
+              break;
+            }
+          }
+          for (PublishedBox box : list) {
+            box.setOpen(open);
+          }
+        }
+      }
+    };
+  }
+
+  private Runnable toggleReviewed() {
+    return new Runnable() {
+     public void run() {
+       header.setReviewed(!header.isReviewed());
+     }
+    };
+  }
+
+  private Runnable flipCursorSide(final CodeMirror cm, final boolean toLeft) {
+    return new Runnable() {
+      public void run() {
+        if (cm.hasActiveLine() && (toLeft && cm == cmB || !toLeft && cm == cmA)) {
+          CodeMirror other = otherCm(cm);
+          other.setCursor(LineCharacter.create(
+              mapper.lineOnOther(
+                  getSideFromCm(cm), cm.getLineNumber(cm.getActiveLine())).getLine()));
+          other.focus();
+        }
+      }
+    };
+  }
+
+  private Runnable maybeNextVimSearch(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.hasVimSearchHighlight()) {
+          CodeMirror.handleVimKey(cm, "n");
+        } else {
+          diffChunkNav(cm, false).run();
+        }
+      }
+    };
+  }
+
+  private Runnable diffChunkNav(final CodeMirror cm, final boolean prev) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        int line = cm.hasActiveLine() ? cm.getLineNumber(cm.getActiveLine()) : 0;
+        int res = Collections.binarySearch(
+                diffChunks,
+                new DiffChunkInfo(getSideFromCm(cm), line, 0, false),
+                getDiffChunkComparator());
+        if (res < 0) {
+          res = -res - (prev ? 1 : 2);
+        }
+
+        res = res + (prev ? -1 : 1);
+        DiffChunkInfo lookUp = diffChunks.get(getWrapAroundDiffChunkIndex(res));
+        // If edit, skip the deletion chunk and set focus on the insertion one.
+        if (lookUp.isEdit() && lookUp.getSide() == DisplaySide.A) {
+          res = res + (prev ? -1 : 1);
+        }
+        DiffChunkInfo target = diffChunks.get(getWrapAroundDiffChunkIndex(res));
+        CodeMirror targetCm = getCmFromSide(target.getSide());
+        targetCm.setCursor(LineCharacter.create(target.getStart()));
+        targetCm.focus();
+        targetCm.scrollToY(
+            targetCm.heightAtLine(target.getStart(), "local") -
+            0.5 * cmB.getScrollbarV().getClientHeight());
+      }
+    };
+  }
+
+  /**
+   * Diff chunks are ordered by their starting lines. If it's a deletion,
+   * use its corresponding line on the revision side for comparison. In
+   * the edit case, put the deletion chunk right before the insertion chunk.
+   * This placement guarantees well-ordering.
+   */
+  private Comparator<DiffChunkInfo> getDiffChunkComparator() {
+    return new Comparator<DiffChunkInfo>() {
+      @Override
+      public int compare(DiffChunkInfo o1, DiffChunkInfo o2) {
+        if (o1.getSide() == o2.getSide()) {
+          return o1.getStart() - o2.getStart();
+        } else if (o1.getSide() == DisplaySide.A) {
+          int comp = mapper.lineOnOther(o1.getSide(), o1.getStart())
+              .getLine() - o2.getStart();
+          return comp == 0 ? -1 : comp;
+        } else {
+          int comp = o1.getStart() -
+              mapper.lineOnOther(o2.getSide(), o2.getStart()).getLine();
+          return comp == 0 ? 1 : comp;
+        }
+      }
+    };
+  }
+
+  private DiffChunkInfo getDiffChunk(DisplaySide side, int line) {
+    int res = Collections.binarySearch(
+        diffChunks,
+        new DiffChunkInfo(side, line, 0, false), // Dummy DiffChunkInfo
+        getDiffChunkComparator());
+    if (res >= 0) {
+      return diffChunks.get(res);
+    } else { // The line might be within a DiffChunk
+      res = -res - 1;
+      if (res > 0) {
+        DiffChunkInfo info = diffChunks.get(res - 1);
+        if (info.getSide() == side && info.getStart() <= line &&
+            line <= info.getEnd()) {
+          return info;
+        }
+      }
+    }
+    return null;
+  }
+
+  private int getWrapAroundDiffChunkIndex(int index) {
+    return (index + diffChunks.size()) % diffChunks.size();
+  }
+
+  void resizePaddingOnOtherSide(DisplaySide mySide, int line) {
+    CodeMirror cm = getCmFromSide(mySide);
+    LineHandle handle = cm.getLineHandle(line);
+    final LinePaddingWidgetWrapper otherWrapper = linePaddingOnOtherSideMap.get(handle);
+    double myChunkHeight = cm.heightAtLine(line + 1) -
+        cm.heightAtLine(line - otherWrapper.getChunkLength() + 1);
+    Element otherPadding = otherWrapper.getElement();
+    int otherPaddingHeight = otherPadding.getOffsetHeight();
+    CodeMirror otherCm = otherCm(cm);
+    int otherLine = otherWrapper.getOtherLine();
+    LineHandle other = otherCm.getLineHandle(otherLine);
+    if (linePaddingOnOtherSideMap.containsKey(other)) {
+      LinePaddingWidgetWrapper myWrapper = linePaddingOnOtherSideMap.get(other);
+      Element myPadding = linePaddingOnOtherSideMap.get(other).getElement();
+      int myPaddingHeight = myPadding.getOffsetHeight();
+      myChunkHeight -= myPaddingHeight;
+      double otherChunkHeight = otherCm.heightAtLine(otherLine + 1) -
+          otherCm.heightAtLine(otherLine - myWrapper.getChunkLength() + 1) -
+          otherPaddingHeight;
+      double delta = myChunkHeight - otherChunkHeight;
+      if (delta > 0) {
+        if (myPaddingHeight != 0) {
+          setHeightInPx(myPadding, 0);
+          myWrapper.getWidget().changed();
+        }
+        if (otherPaddingHeight != delta) {
+          setHeightInPx(otherPadding, delta);
+          otherWrapper.getWidget().changed();
+        }
+      } else {
+        if (myPaddingHeight != -delta) {
+          setHeightInPx(myPadding, -delta);
+          myWrapper.getWidget().changed();
+        }
+        if (otherPaddingHeight != 0) {
+          setHeightInPx(otherPadding, 0);
+          otherWrapper.getWidget().changed();
+        }
+      }
+    } else if (otherPaddingHeight != myChunkHeight) {
+      setHeightInPx(otherPadding, myChunkHeight);
+      otherWrapper.getWidget().changed();
+    }
+  }
+
+  // TODO: Maybe integrate this with PaddingManager.
+  private RenderLineHandler resizeLinePadding(final DisplaySide side) {
+    return new RenderLineHandler() {
+      @Override
+      public void handle(final CodeMirror instance, final LineHandle handle,
+          Element element) {
+        if (lineActiveBoxMap.containsKey(handle)) {
+          lineActiveBoxMap.get(handle).resizePaddingWidget();
+        }
+        if (linePaddingOnOtherSideMap.containsKey(handle)) {
+          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+            @Override
+            public void execute() {
+              resizePaddingOnOtherSide(side, instance.getLineNumber(handle));
+            }
+          });
+        }
+      }
+    };
+  }
+
+  void resizeCodeMirror() {
+    int rest = Gerrit.getHeaderFooterHeight()
+        + header.getOffsetHeight()
+        + diffTable.getHeaderHeight()
+        + 10; // Estimate
+    int h = Window.getClientHeight() - rest;
+    cmA.setHeight(h);
+    cmB.setHeight(h);
+    cmA.refresh();
+    cmB.refresh();
+    diffTable.sidePanel.adjustGutters(cmB);
+  }
+
+  static void setHeightInPx(Element ele, double height) {
+    ele.getStyle().setHeight(height, Unit.PX);
+  }
+
+  private String getContentType(DiffInfo.FileMeta meta) {
+    return pref.isSyntaxHighlighting()
+          && meta != null
+          && meta.content_type() != null
+        ? ModeInjector.getContentType(meta.content_type())
+        : null;
+  }
+
+  CodeMirror getCmA() {
+    return cmA;
+  }
+
+  CodeMirror getCmB() {
+    return cmB;
+  }
+
+  private void prefetchNextFile() {
+    String nextPath = header.getNextPath();
+    if (nextPath != null) {
+      DiffApi.diff(revision, nextPath)
+        .base(base)
+        .wholeFile()
+        .intraline(pref.isIntralineDifference())
+        .ignoreWhitespace(pref.getIgnoreWhitespace())
+        .get(new AsyncCallback<DiffInfo>() {
+          @Override
+          public void onSuccess(DiffInfo info) {
+            new ModeInjector()
+              .add(getContentType(info.meta_a()))
+              .add(getContentType(info.meta_b()))
+              .inject(CallbackGroup.<Void> emptyCallback());
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml
new file mode 100644
index 0000000..1bc707a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:d='urn:import:com.google.gerrit.client.diff'>
+  <g:FlowPanel>
+    <d:Header ui:field='header'/>
+    <d:DiffTable ui:field='diffTable'/>
+  </g:FlowPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.java
new file mode 100644
index 0000000..6a19b30
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.java
@@ -0,0 +1,165 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Label;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.LineCharacter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** The Widget that handles the scrollbar gutters */
+class SidePanel extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, SidePanel> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface SidePanelStyle extends CssResource {
+    String gutter();
+    String halfGutter();
+    String comment();
+    String draft();
+    String insert();
+    String delete();
+  }
+
+  enum GutterType {
+    COMMENT, DRAFT, INSERT, DELETE, EDIT;
+  }
+
+  @UiField
+  SidePanelStyle style;
+
+  private List<GutterWrapper> gutters;
+  private CodeMirror cmB;
+
+  SidePanel() {
+    initWidget(uiBinder.createAndBindUi(this));
+    this.gutters = new ArrayList<GutterWrapper>();
+  }
+
+  GutterWrapper addGutter(CodeMirror cm, int line, GutterType type) {
+    Label gutter = new Label();
+    GutterWrapper info = new GutterWrapper(this, gutter, cm, line, type);
+    adjustGutter(info);
+    gutter.addStyleName(style.gutter());
+    switch (type) {
+      case COMMENT:
+        gutter.addStyleName(style.comment());
+        break;
+      case DRAFT:
+        gutter.addStyleName(style.draft());
+        gutter.setText("*");
+        break;
+      case INSERT:
+        gutter.addStyleName(style.insert());
+        break;
+      case DELETE:
+        gutter.addStyleName(style.delete());
+        break;
+      case EDIT:
+        gutter.addStyleName(style.insert());
+        Label labelLeft = new Label();
+        labelLeft.addStyleName(style.halfGutter());
+        gutter.getElement().appendChild(labelLeft.getElement());
+    }
+    ((HTMLPanel) getWidget()).add(gutter);
+    gutters.add(info);
+    return info;
+  }
+
+  void adjustGutters(CodeMirror cmB) {
+    this.cmB = cmB;
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        for (GutterWrapper info : gutters) {
+          adjustGutter(info);
+        }
+      }
+    });
+  }
+
+  private void adjustGutter(GutterWrapper wrapper) {
+    if (cmB == null) {
+      return;
+    }
+    final CodeMirror cm = wrapper.cm;
+    final int line = wrapper.line;
+    Label gutter = wrapper.gutter;
+    final double height = cm.heightAtLine(line, "local");
+    final double scrollbarHeight = cmB.getScrollbarV().getClientHeight();
+    double top = height / (double) cmB.getSizer().getClientHeight() *
+        scrollbarHeight +
+        cmB.getScrollbarV().getAbsoluteTop();
+    if (top == 0) {
+      top = -10;
+    }
+    gutter.getElement().getStyle().setTop(top, Unit.PX);
+    wrapper.replaceClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        cm.setCursor(LineCharacter.create(line));
+        cm.scrollToY(height - 0.5 * scrollbarHeight);
+        cm.focus();
+      }
+    });
+  }
+
+  void removeGutter(GutterWrapper wrapper) {
+    gutters.remove(wrapper);
+  }
+
+  static class GutterWrapper {
+    private SidePanel host;
+    private Label gutter;
+    private CodeMirror cm;
+    private int line;
+    private HandlerRegistration regClick;
+
+    GutterWrapper(SidePanel host, Label anchor, CodeMirror cm, int line,
+        GutterType type) {
+      this.host = host;
+      this.gutter = anchor;
+      this.cm = cm;
+      this.line = line;
+    }
+
+    private void replaceClickHandler(ClickHandler newHandler) {
+      if (regClick != null) {
+        regClick.removeHandler();
+      }
+      regClick = gutter.addClickHandler(newHandler);
+    }
+
+    void remove() {
+      gutter.removeFromParent();
+      host.removeGutter(this);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.ui.xml
new file mode 100644
index 0000000..e2b56f3f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.ui.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:style type='com.google.gerrit.client.diff.SidePanel.SidePanelStyle'>
+    .gutter {
+      cursor: pointer;
+      position: fixed;
+      height: 3px;
+      width: 10px;
+      border: 1px solid black;
+    }
+    .halfGutter {
+      cursor: pointer;
+      position: fixed;
+      height: 3px;
+      width: 5px;
+      background-color: #faa;
+    }
+    .comment, .draft {
+      background-color: #e5ecf9;
+    }
+    .draft {
+      text-align: center;
+      font-size: small;
+      line-height: 0.5;
+      color: inherit !important;
+      text-decoration: none !important;
+    }
+    .delete {
+      background-color: #faa;
+    }
+    .insert {
+      background-color: #9f9;
+    }
+  </ui:style>
+  <g:HTMLPanel/>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
new file mode 100644
index 0000000..cc8ac2e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -0,0 +1,191 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.TextMarker;
+import net.codemirror.lib.TextMarker.FromTo;
+
+/** The Widget that handles expanding of skipped lines */
+class SkipBar extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, SkipBar> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+  private static final int NUM_ROWS_TO_EXPAND = 10;
+  private static final int UP_DOWN_THRESHOLD = 30;
+  private static final Configuration MARKER_CONFIG = Configuration.create()
+      .set("collapsed", true)
+      .set("inclusiveLeft", true)
+      .set("inclusiveRight", true);
+
+  private LineWidget widget;
+
+  interface SkipBarStyle extends CssResource {
+    String noExpand();
+  }
+
+  @UiField(provided=true)
+  Anchor skipNum;
+
+  @UiField(provided=true)
+  Anchor upArrow;
+
+  @UiField(provided=true)
+  Anchor downArrow;
+
+  @UiField
+  SkipBarStyle style;
+
+  private TextMarker marker;
+  private SkipBar otherBar;
+  private CodeMirror cm;
+  private int numSkipLines;
+
+  SkipBar(CodeMirror cmInstance) {
+    cm = cmInstance;
+    skipNum = new Anchor(true);
+    upArrow = new Anchor(true);
+    downArrow = new Anchor(true);
+    initWidget(uiBinder.createAndBindUi(this));
+    addDomHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        cm.focus();
+      }
+    }, ClickEvent.getType());
+  }
+
+  void setWidget(LineWidget lineWidget) {
+    widget = lineWidget;
+    Scheduler.get().scheduleDeferred(new ScheduledCommand(){
+      @Override
+      public void execute() {
+        getElement().getStyle().setPaddingLeft(
+            cm.getGutterElement().getOffsetWidth(), Unit.PX);
+      }
+    });
+  }
+
+  void setMarker(TextMarker marker, int length) {
+    this.marker = marker;
+    numSkipLines = length;
+    skipNum.setText(Integer.toString(length));
+    if (checkAndUpdateArrows()) {
+      upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
+      downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
+    }
+  }
+
+  static void link(SkipBar barA, SkipBar barB) {
+    barA.otherBar = barB;
+    barB.otherBar = barA;
+  }
+
+  private void updateSkipNum() {
+    numSkipLines -= NUM_ROWS_TO_EXPAND;
+    skipNum.setText(String.valueOf(numSkipLines));
+    checkAndUpdateArrows();
+  }
+
+  private boolean checkAndUpdateArrows() {
+    if (numSkipLines <= UP_DOWN_THRESHOLD) {
+      addStyleName(style.noExpand());
+      return false;
+    }
+    return true;
+  }
+
+  private void clearMarkerAndWidget() {
+    marker.clear();
+    assert (widget != null);
+    widget.clear();
+  }
+
+  private void expandAll() {
+    clearMarkerAndWidget();
+    removeFromParent();
+    cm.focus();
+  }
+
+  private void expandBefore() {
+    FromTo fromTo = marker.find();
+    int oldStart = fromTo.getFrom().getLine();
+    int newStart = oldStart + NUM_ROWS_TO_EXPAND;
+    int end = fromTo.getTo().getLine();
+    clearMarkerAndWidget();
+    marker = cm.markText(
+        CodeMirror.pos(newStart, 0), CodeMirror.pos(end), MARKER_CONFIG);
+    Configuration config = Configuration.create()
+        .set("coverGutter", true)
+        .set("noHScroll", true);
+    setWidget(cm.addLineWidget(newStart - 1, getElement(), config));
+    updateSkipNum();
+    cm.focus();
+  }
+
+  private void expandAfter() {
+    FromTo fromTo = marker.find();
+    int start = fromTo.getFrom().getLine();
+    int oldEnd = fromTo.getTo().getLine();
+    int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
+    marker.clear();
+    marker = cm.markText(
+        CodeMirror.pos(start, 0), CodeMirror.pos(newEnd), MARKER_CONFIG);
+    if (start == 0) { // First line workaround
+      Configuration config = Configuration.create()
+          .set("coverGutter", true)
+          .set("noHScroll", true)
+          .set("above", true);
+      widget.clear();
+      setWidget(cm.addLineWidget(newEnd + 1, getElement(), config));
+    }
+    updateSkipNum();
+    cm.focus();
+  }
+
+  @UiHandler("skipNum")
+  void onExpandAll(ClickEvent e) {
+    otherBar.expandAll();
+    expandAll();
+  }
+
+  @UiHandler("upArrow")
+  void onExpandBefore(ClickEvent e) {
+    otherBar.expandBefore();
+    expandBefore();
+  }
+
+  @UiHandler("downArrow")
+  void onExpandAfter(ClickEvent e) {
+    otherBar.expandAfter();
+    expandAfter();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
new file mode 100644
index 0000000..f5c6719
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:style type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
+    .skipBar {
+      background-color: #def;
+      height: 1.3em;
+      overflow: hidden;
+    }
+    .text {
+      display: table;
+      margin: 0 auto;
+      color: #777;
+      font-style: italic;
+      overflow: hidden;
+    }
+    .anchor {
+      color: inherit;
+      text-decoration: none;
+    }
+    .noExpand .arrow {
+      display: none;
+    }
+    .arrow {
+      font-family: Arial Unicode MS, sans-serif;
+    }
+  </ui:style>
+  <g:HTMLPanel addStyleNames='{style.skipBar}'>
+  <div class='{style.text}'>
+    <ui:msg>
+      <g:Anchor ui:field='upArrow' addStyleNames='{style.arrow} {style.anchor}' />
+      <span><ui:msg>... skipped </ui:msg></span>
+      <g:Anchor ui:field='skipNum' addStyleNames='{style.anchor}' />
+      <span><ui:msg> common lines ...</ui:msg></span>
+      <g:Anchor ui:field='downArrow' addStyleNames=' {style.arrow} {style.anchor}' />
+    </ui:msg>
+  </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
new file mode 100644
index 0000000..7071e7f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+
+class UpToChangeCommand2 extends KeyCommand {
+  private final PatchSet.Id revision;
+
+  UpToChangeCommand2(PatchSet.Id revision, int mask, int key) {
+    super(mask, key, PatchUtil.C.upToChange());
+    this.revision = revision;
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    Gerrit.display(PageLinks.toChange(
+        revision.getParentKey(),
+        String.valueOf(revision.get())));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy100.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy100.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png
new file mode 100644
index 0000000..88b59d8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java
new file mode 100644
index 0000000..e78c5ce
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.extensions;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+public class TopMenu extends JavaScriptObject {
+
+  protected TopMenu() {
+  }
+
+  public final native String getName() /*-{ return this.name; }-*/;
+
+  public final native JsArray<TopMenuItem> getItems() /*-{ return this.items; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java
new file mode 100644
index 0000000..22bb981
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.extensions;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class TopMenuItem extends JavaScriptObject {
+  public final native String getName() /*-{ return this.name; }-*/;
+  public final native String getUrl() /*-{ return this.url; }-*/;
+  public final native String getTarget() /*-{ return this.target; }-*/;
+  public final native String getId() /*-{ return this.id; }-*/;
+
+  protected TopMenuItem() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
new file mode 100644
index 0000000..4413603
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.extensions;
+
+import com.google.gwt.core.client.JsArray;
+
+public class TopMenuList extends JsArray<TopMenu> {
+
+  protected TopMenuList() {
+  }
+}
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 5f16af6..48cabe7 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
@@ -39,12 +39,15 @@
 /** Override various GWT defaults */
 .gerritTopMenu {
   font-size: 9pt;
-  padding-top: 5px;
   padding-left: 5px;
   padding-right: 5px;
   background: transparent;
 }
 
+body, table td, select {
+  font-family: norm-font;
+}
+
 .gerritBody {
   font-size: small;
   padding-left: 5px;
@@ -62,12 +65,11 @@
   text-decoration: underline;
 }
 
-.version,
-.keyhelp {
+#gerrit_btmmenu {
+  clear: both;
   color: #a0adcc;
-  right: 0;
-  padding-right: 10px;
   text-align: right;
+  padding-right: 10px;
 }
 
 .version a,
@@ -339,6 +341,7 @@
   border: 1px solid black;
   background: white;
   box-shadow: 3px 3px 5px #888;
+  z-index: 200;
 }
 .searchPanel {
   white-space: nowrap;
@@ -352,27 +355,23 @@
   margin-left: 2px;
   padding: 3px 6px;
 }
-
-/** RPC Status **/
-.rpcStatusPanel {
-  position: absolute;
-  left: 50%;
-  float: left;
-  top: 6px;
+.suggestBoxPopup {
+  z-index: 200;
 }
 
+/** RPC Status **/
 .rpcStatus {
   position: fixed;
+  top: 6px;
+  left: 50%;
   padding-top: 4px;
   padding-bottom: 4px;
   padding-left: 10px;
   padding-right: 10px;
   text-align: center;
   font-weight: bold;
-}
-
-.rpcStatusLoading {
   background: #FFF1A8;
+  z-index: 10;
 }
 
 
@@ -385,9 +384,11 @@
   color: backgroundColor;
   font-size: 15px;
   font-family: verdana;
+  z-index: 200;
 }
 .errorDialogGlass {
   opacity: 0.75;
+  z-index: 200;
 }
 @if user.agent safari {
   .errorDialogGlass {
@@ -589,6 +590,9 @@
 .changeTable .dataCell.singleLine {
   white-space: nowrap;
 }
+.changeTable .dataCell.labelNotApplicable {
+ background: #F5F5F5;
+}
 .changeTable .iconHeader {
   border-top: 1px solid backgroundColor;
   border-bottom: 1px solid backgroundColor;
@@ -657,7 +661,7 @@
 .patchContentTable td {
   padding-top: 0;
   padding-bottom: 0;
-  font-size: 8pt;
+  font-size: 9pt;
   font-family: mono-font;
 }
 
@@ -1086,6 +1090,51 @@
   padding-left: 4em;
   border-left: none;
 }
+
+.downloadBox {
+  min-width: 580px;
+  margin: 5px;
+}
+.downloadBoxTable {
+  border-spacing: 0;
+  width: 100%;
+}
+.downloadBoxTableCommandColumn {
+  text-align: left;
+  font-weight: normal;
+  white-space: nowrap;
+  max-height: 18px;
+  width: 80px;
+  padding-right: 5px;
+}
+.downloadBoxSpacer {
+  margin-left: 5px;
+  margin-right: 5px;
+}
+.downloadBoxScheme {
+  float: right;
+}
+.downloadBoxCopyLabel {
+  font-size: smaller;
+  font-family: monospace;
+}
+.downloadBoxCopyLabel span {
+  width: 500px;
+  white-space: nowrap;
+  display: inline-block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.downloadBoxCopyLabel .gwt-TextBox {
+  padding: 0;
+  margin: 0;
+  border: 0;
+  max-height: 18px;
+  width: 500px;
+}
+.downloadBoxCopyLabel div {
+  float: right;
+}
 td.downloadLinkListCell {
   padding: 0px;
 }
@@ -1374,7 +1423,9 @@
   font-family: mono-font;
   font-size: small;
 }
-
+.projectActions {
+  margin-bottom: 10px;
+}
 
 /** PublishCommentsScreen **/
 .publishCommentsScreen .smallHeading {
@@ -1534,3 +1585,11 @@
 .projectFilterLabel {
   margin-right: 5px;
 }
+.projectNameColumn {
+  min-width: 300px;
+}
+
+/** ProjectSettings */
+.maxObjectSizeLimitPanel td {
+  padding-right: 5px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
new file mode 100644
index 0000000..4811e59
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.groups;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.URL;
+
+public class GroupBaseInfo extends JavaScriptObject {
+  public final AccountGroup.UUID getGroupUUID() {
+    return new AccountGroup.UUID(URL.decodeQueryString(id()));
+  }
+
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+
+  protected GroupBaseInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
index f529990..f1e4e87 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -20,17 +20,11 @@
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.http.client.URL;
 
-public class GroupInfo extends JavaScriptObject {
+public class GroupInfo extends GroupBaseInfo {
   public final AccountGroup.Id getGroupId() {
     return new AccountGroup.Id(group_id());
   }
 
-  public final AccountGroup.UUID getGroupUUID() {
-    return new AccountGroup.UUID(URL.decodeQueryString(id()));
-  }
-
-  public final native String id() /*-{ return this.id; }-*/;
-  public final native String name() /*-{ return this.name; }-*/;
   public final native GroupOptionsInfo options() /*-{ return this.options; }-*/;
   public final native String description() /*-{ return this.description; }-*/;
   public final native String url() /*-{ return this.url; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index c07783d..fd3e35c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -66,6 +66,7 @@
 
 import org.eclipse.jgit.diff.Edit;
 
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -523,9 +524,10 @@
             throw new RuntimeException("unexpected file id " + file);
         }
 
-        final PatchLineComment newComment =
-            new PatchLineComment(new PatchLineComment.Key(parentKey, null),
-                line, Gerrit.getUserAccount().getId(), null);
+        final PatchLineComment newComment = new PatchLineComment(
+            new PatchLineComment.Key(parentKey, null), line,
+            Gerrit.getUserAccount().getId(), null,
+            new Timestamp(System.currentTimeMillis()));
         newComment.setSide(side);
         newComment.setMessage("");
 
@@ -969,7 +971,8 @@
       PatchLineComment newComment =
           new PatchLineComment(new PatchLineComment.Key(comment.getKey()
               .getParentKey(), null), comment.getLine(), Gerrit
-              .getUserAccount().getId(), comment.getKey().get());
+              .getUserAccount().getId(), comment.getKey().get(),
+              new Timestamp(System.currentTimeMillis()));
       newComment.setSide(comment.getSide());
       return newComment;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 8c9c56b..908801b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -40,14 +40,18 @@
   String illegalNumberOfColumns();
 
   String upToChange();
+  String openReply();
   String linePrev();
   String lineNext();
   String chunkPrev();
   String chunkNext();
+  String chunkPrev2();
+  String chunkNext2();
   String commentPrev();
   String commentNext();
   String fileList();
   String expandComment();
+  String expandAllCommentsOnCurrentLine();
 
   String toggleReviewed();
   String markAsReviewedAndGoToNext();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 5acdb5f..a1b6192 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -22,14 +22,18 @@
 illegalNumberOfColumns = The number of columns cannot be zero or negative
 
 upToChange = Up to change
+openReply = Reply and score
 linePrev = Previous line
 lineNext = Next line
 chunkPrev = Previous diff chunk or comment
 chunkNext = Next diff chunk or comment
+chunkPrev2 = Previous diff chunk
+chunkNext2 = Next diff chunk or search result
 commentPrev = Previous comment
 commentNext = Next comment
 fileList = Browse files in patch set
 expandComment = Expand or collapse comment
+expandAllCommentsOnCurrentLine = Expand or collapse all comments on current line
 
 toggleReviewed = Toggle the reviewed flag
 markAsReviewedAndGoToNext = Mark patch as reviewed and go to next unreviewed patch
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index 200562a..fa2eb32 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -50,6 +50,7 @@
 public abstract class PatchScreen extends Screen implements
     CommentEditorContainer {
   static final PrettyFactory PRETTY = ClientSideFormatter.FACTORY;
+  static final short LARGE_FILE_CONTEXT = 100;
 
   public static class SideBySide extends PatchScreen {
     public SideBySide(final Patch.Key id, final int patchIndex,
@@ -386,7 +387,7 @@
           }
         }));
     PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB,
-        settingsPanel.getValue(), cb.addGwtjsonrpc(
+        settingsPanel.getValue(), cb.addFinal(
             new ScreenLoadCallback<PatchScript>(this) {
               @Override
               protected void preDisplay(final PatchScript result) {
@@ -460,6 +461,19 @@
       setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
 
+    if (script.isHugeFile()) {
+      AccountDiffPreference dp = script.getDiffPrefs();
+      int context = dp.getContext();
+      if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+        context = Short.MAX_VALUE;
+      } else if (context > Short.MAX_VALUE) {
+        context = Short.MAX_VALUE;
+      }
+      dp.setContext((short) Math.min(context, LARGE_FILE_CONTEXT));
+      dp.setSyntaxHighlighting(false);
+      script.setDiffPrefs(dp);
+    }
+
     contentTable.display(patchKey, idSideA, idSideB, script, patchSetDetail);
     contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
     contentTable.finishDisplay();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 8e76e47..1153999 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -23,6 +23,9 @@
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.OptionElement;
+import com.google.gwt.dom.client.SelectElement;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -157,6 +160,21 @@
     } else {
       syntaxHighlighting.setValue(false);
     }
+
+    NodeList<OptionElement> options =
+        context.getElement().<SelectElement>cast().getOptions();
+    // WHOLE_FILE_CONTEXT is the last option in the list.
+    int lastIndex = options.getLength() - 1;
+    OptionElement currOption = options.getItem(lastIndex);
+    if (enableSmallFileFeatures) {
+      currOption.setDisabled(false);
+    } else {
+      currOption.setDisabled(true);
+      if (context.getSelectedIndex() == lastIndex) {
+        // Select the next longest context from WHOLE_FILE_CONTEXT
+        context.setSelectedIndex(lastIndex - 1);
+      }
+    }
     toggleEnabledStatus(save.isEnabled());
   }
 
@@ -314,7 +332,7 @@
     if (0 <= sel) {
       return Short.parseShort(context.getValue(sel));
     }
-    return (short) getValue().getContext();
+    return getValue().getContext();
   }
 
   private void setContext(int ctx) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
index df12b70..0ac06d0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
@@ -44,7 +44,7 @@
   interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {
   }
 
-  private static Binder uiBinder = GWT.create(Binder.class);
+  private static final Binder uiBinder = GWT.create(Binder.class);
 
   interface BoxStyle extends CssResource {
     String selected();
@@ -133,7 +133,7 @@
 
     if (idActive == null && side == Side.A) {
       links.get(0).setStyleName(style.selected());
-    } else {
+    } else if (idActive != null) {
       links.get(idActive.get()).setStyleName(style.selected());
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
index 5ebeaf5..e0c5143 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -52,6 +52,7 @@
 
   private SparseHtmlFile a;
   private SparseHtmlFile b;
+  private boolean isHugeFile;
   protected boolean isFileCommentBorderRowExist;
 
   protected void createFileCommentEditorOnSideA() {
@@ -94,6 +95,7 @@
   protected void render(final PatchScript script, final PatchSetDetail detail) {
     final ArrayList<Object> lines = new ArrayList<Object>();
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
+    isHugeFile = script.isHugeFile();
     allocateTableHeader(script, nc);
     lines.add(null);
     if (!isDisplayBinary) {
@@ -209,7 +211,7 @@
         for (int row = 0; row < lines.size(); row++) {
           setRowItem(row, lines.get(row));
           if (lines.get(row) instanceof SkippedLine) {
-            createSkipLine(row, (SkippedLine) lines.get(row), script.getA().isWholeFile());
+            createSkipLine(row, (SkippedLine) lines.get(row), isHugeFile);
           }
         }
       }
@@ -540,18 +542,16 @@
 
     if (numRows > 0) {
       line.incrementStart(numRows);
-      // If we got here, we must have the whole file anyway.
-      createSkipLine(row + loopTo, line, true);
+      createSkipLine(row + loopTo, line, isHugeFile);
     } else if (numRows < 0) {
       line.reduceSize(-numRows);
-      // If we got here, we must have the whole file anyway.
-      createSkipLine(row, line, true);
+      createSkipLine(row, line, isHugeFile);
     } else {
       table.removeRow(row + loopTo);
     }
   }
 
-  private void createSkipLine(int row, SkippedLine line, boolean isWholeFile) {
+  private void createSkipLine(int row, SkippedLine line, boolean isHugeFile) {
     FlowPanel p = new FlowPanel();
     InlineLabel l1 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionStart() + " ");
     InlineLabel l2 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
@@ -560,7 +560,7 @@
     all.addClickHandler(expandAllListener);
     all.setStyleName(Gerrit.RESOURCES.css().skipLine());
 
-    if (line.getSize() > 30 && isWholeFile) {
+    if (line.getSize() > 30) {
       // Only show the expand before/after if skipped more than 30 lines.
       Anchor b = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND), true);
       Anchor a = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND), true);
@@ -573,16 +573,16 @@
 
       p.add(b);
       p.add(l1);
-      p.add(all);
+      if (isHugeFile) {
+        p.add(new InlineLabel(" " + line.getSize() + " "));
+      } else {
+        p.add(all);
+      }
       p.add(l2);
       p.add(a);
-    } else if (isWholeFile) {
-      p.add(l1);
-      p.add(all);
-      p.add(l2);
     } else {
       p.add(l1);
-      p.add(new InlineLabel(" " + line.getSize() + " "));
+      p.add(all);
       p.add(l2);
     }
     table.setWidget(row, 1, p);
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
new file mode 100644
index 0000000..3f79afe
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class BranchInfo extends JavaScriptObject {
+  public final String getShortName() {
+    return ref().startsWith(Branch.R_HEADS)
+        ? ref().substring(Branch.R_HEADS.length())
+        : ref();
+  }
+
+  public final native String ref() /*-{ return this.ref; }-*/;
+  public final native String revision() /*-{ return this.revision; }-*/;
+  public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
+
+  protected BranchInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index 522d348..c7177e9 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
@@ -14,7 +14,11 @@
 
 package com.google.gerrit.client.projects;
 
+import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwtexpui.safehtml.client.FindReplace;
@@ -25,26 +29,44 @@
 import java.util.List;
 
 public class ConfigInfo extends JavaScriptObject {
-  public final native JavaScriptObject has_require_change_id()
-  /*-{ return this.hasOwnProperty('require_change_id'); }-*/;
-  public final native boolean require_change_id()
+
+  public final native String description()
+  /*-{ return this.description }-*/;
+
+  public final native InheritedBooleanInfo require_change_id()
   /*-{ return this.require_change_id; }-*/;
 
-  public final native JavaScriptObject has_use_content_merge()
-  /*-{ return this.hasOwnProperty('use_content_merge'); }-*/;
-  public final native boolean use_content_merge()
+  public final native InheritedBooleanInfo use_content_merge()
   /*-{ return this.use_content_merge; }-*/;
 
-  public final native JavaScriptObject has_use_contributor_agreements()
-  /*-{ return this.hasOwnProperty('use_contributor_agreements'); }-*/;
-  public final native boolean use_contributor_agreements()
+  public final native InheritedBooleanInfo use_contributor_agreements()
   /*-{ return this.use_contributor_agreements; }-*/;
 
-  public final native JavaScriptObject has_use_signed_off_by()
-  /*-{ return this.hasOwnProperty('use_signed_off_by'); }-*/;
-  public final native boolean use_signed_off_by()
+  public final native InheritedBooleanInfo use_signed_off_by()
   /*-{ return this.use_signed_off_by; }-*/;
 
+  public final SubmitType submit_type() {
+    return SubmitType.valueOf(submit_typeRaw());
+  }
+
+  public final native NativeMap<ActionInfo> actions()
+  /*-{ return this.actions; }-*/;
+
+  private final native String submit_typeRaw()
+  /*-{ return this.submit_type }-*/;
+
+  public final Project.State state() {
+    if (stateRaw() == null) {
+      return Project.State.ACTIVE;
+    }
+    return Project.State.valueOf(stateRaw());
+  }
+  private final native String stateRaw()
+  /*-{ return this.state }-*/;
+
+  public final native MaxObjectSizeLimitInfo max_object_size_limit()
+  /*-{ return this.max_object_size_limit; }-*/;
+
   private final native NativeMap<CommentLinkInfo> commentlinks0()
   /*-{ return this.commentlinks; }-*/;
   final List<FindReplace> commentlinks() {
@@ -80,4 +102,40 @@
     protected CommentLinkInfo() {
     }
   }
+
+  public static class InheritedBooleanInfo extends JavaScriptObject {
+    public static InheritedBooleanInfo create() {
+      return (InheritedBooleanInfo) createObject();
+    }
+
+    public final native boolean value()
+    /*-{ return this.value ? true : false; }-*/;
+
+    public final native boolean inherited_value()
+    /*-{ return this.inherited_value ? true : false; }-*/;
+
+    public final InheritableBoolean configured_value() {
+      return InheritableBoolean.valueOf(configured_valueRaw());
+    }
+    private final native String configured_valueRaw()
+    /*-{ return this.configured_value }-*/;
+
+    public final void setConfiguredValue(InheritableBoolean v) {
+      setConfiguredValueRaw(v.name());
+    }
+    public final native void setConfiguredValueRaw(String v)
+    /*-{ if(v)this.configured_value=v; }-*/;
+
+    protected InheritedBooleanInfo() {
+    }
+  }
+
+  public static class MaxObjectSizeLimitInfo extends JavaScriptObject {
+    public final native String value() /*-{ return this.value; }-*/;
+    public final native String inherited_value() /*-{ return this.inherited_value; }-*/;
+    public final native String configured_value() /*-{ return this.configured_value }-*/;
+
+    protected MaxObjectSizeLimitInfo() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
index 16406f4..c22b007 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.client.projects;
 
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -24,7 +27,8 @@
 
 /** Cache of {@link ConfigInfo} objects by project name. */
 public class ConfigInfoCache {
-  private static final int LIMIT = 25;
+  private static final int PROJECT_LIMIT = 25;
+  private static final int CHANGE_LIMIT = 100;
   private static final ConfigInfoCache instance =
       GWT.create(ConfigInfoCache.class);
 
@@ -49,36 +53,74 @@
   }
 
   public static void get(Project.NameKey name, AsyncCallback<Entry> cb) {
-    instance.getImpl(name, cb);
+    instance.getImpl(name.get(), cb);
+  }
+
+  public static void get(Change.Id changeId, AsyncCallback<Entry> cb) {
+    instance.getImpl(changeId.get(), cb);
+  }
+
+  public static void add(ChangeInfo info) {
+    instance.changeToProject.put(info.legacy_id().get(), info.project());
   }
 
   private final LinkedHashMap<String, Entry> cache;
+  private final LinkedHashMap<Integer, String> changeToProject;
 
   protected ConfigInfoCache() {
-    cache = new LinkedHashMap<String, Entry>(LIMIT) {
+    cache = new LinkedHashMap<String, Entry>(PROJECT_LIMIT) {
       private static final long serialVersionUID = 1L;
 
       @Override
       protected boolean removeEldestEntry(
           Map.Entry<String, ConfigInfoCache.Entry> e) {
-        return size() > LIMIT;
+        return size() > PROJECT_LIMIT;
+      }
+    };
+
+    changeToProject = new LinkedHashMap<Integer, String>(CHANGE_LIMIT) {
+      private static final long serialVersionUID = 1L;
+
+      @Override
+      protected boolean removeEldestEntry(Map.Entry<Integer, String> e) {
+        return size() > CHANGE_LIMIT;
       }
     };
   }
 
-  private void getImpl(final Project.NameKey name,
-      final AsyncCallback<Entry> cb) {
-    Entry e = cache.get(name.get());
+  private void getImpl(final String name, final AsyncCallback<Entry> cb) {
+    Entry e = cache.get(name);
     if (e != null) {
       cb.onSuccess(e);
       return;
     }
-    ProjectApi.config(name).get(new AsyncCallback<ConfigInfo>() {
+    ProjectApi.getConfig(new Project.NameKey(name),
+        new AsyncCallback<ConfigInfo>() {
+          @Override
+          public void onSuccess(ConfigInfo result) {
+            Entry e = new Entry(result);
+            cache.put(name, e);
+            cb.onSuccess(e);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            cb.onFailure(caught);
+          }
+        });
+  }
+
+  private void getImpl(final Integer id, final AsyncCallback<Entry> cb) {
+    String name = changeToProject.get(id);
+    if (name != null) {
+      getImpl(name, cb);
+      return;
+    }
+    ChangeApi.change(id).get(new AsyncCallback<ChangeInfo>() {
       @Override
-      public void onSuccess(ConfigInfo result) {
-        Entry e = new Entry(result);
-        cache.put(name.get(), e);
-        cb.onSuccess(e);
+      public void onSuccess(ChangeInfo result) {
+        changeToProject.put(id, result.project());
+        getImpl(result.project(), cb);
       }
 
       @Override
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 be133c5..62dbf1c 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
@@ -14,27 +14,121 @@
 package com.google.gerrit.client.projects;
 
 import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
+import java.util.Set;
+
 public class ProjectApi {
   /** Create a new project */
   public static void createProject(String projectName, String parent,
       Boolean createEmptyCcommit, Boolean permissionsOnly,
-      AsyncCallback<VoidResult> asyncCallback) {
+      AsyncCallback<VoidResult> cb) {
     ProjectInput input = ProjectInput.create();
     input.setName(projectName);
     input.setParent(parent);
     input.setPermissionsOnly(permissionsOnly);
     input.setCreateEmptyCommit(createEmptyCcommit);
     new RestApi("/projects/").id(projectName).ifNoneMatch()
-        .put(input, asyncCallback);
+        .put(input, cb);
   }
 
-  static RestApi config(Project.NameKey name) {
-    return new RestApi("/projects/").id(name.get()).view("config");
+  /** Create a new branch */
+  public static void createBranch(Project.NameKey name, String ref,
+      String revision, AsyncCallback<BranchInfo> cb) {
+    BranchInput input = BranchInput.create();
+    input.setRevision(revision);
+    project(name).view("branches").id(ref).ifNoneMatch().put(input, cb);
+  }
+
+  /** Retrieve all visible branches of the project */
+  public static void getBranches(Project.NameKey name,
+      AsyncCallback<JsArray<BranchInfo>> cb) {
+    project(name).view("branches").get(cb);
+  }
+
+  /**
+   * Delete branches. For each branch to be deleted a separate DELETE request is
+   * fired to the server. The {@code onSuccess} method of the provided callback
+   * is invoked once after all requests succeeded. If any request fails the
+   * callbacks' {@code onFailure} method is invoked. In a failure case it can be
+   * that still some of the branches were successfully deleted.
+   */
+  public static void deleteBranches(Project.NameKey name,
+      Set<String> refs, AsyncCallback<VoidResult> cb) {
+    CallbackGroup group = new CallbackGroup();
+    for (String ref : refs) {
+      project(name).view("branches").id(ref)
+          .delete(group.add(cb));
+      cb = CallbackGroup.emptyCallback();
+    }
+    group.done();
+  }
+
+  public static void getConfig(Project.NameKey name,
+      AsyncCallback<ConfigInfo> cb) {
+    project(name).view("config").get(cb);
+  }
+
+  public static void setConfig(Project.NameKey name, String description,
+      InheritableBoolean useContributorAgreements,
+      InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
+      InheritableBoolean requireChangeId, String maxObjectSizeLimit,
+      SubmitType submitType, Project.State state, AsyncCallback<ConfigInfo> cb) {
+    ConfigInput in = ConfigInput.create();
+    in.setDescription(description);
+    in.setUseContributorAgreements(useContributorAgreements);
+    in.setUseContentMerge(useContentMerge);
+    in.setUseSignedOffBy(useSignedOffBy);
+    in.setRequireChangeId(requireChangeId);
+    in.setMaxObjectSizeLimit(maxObjectSizeLimit);
+    in.setSubmitType(submitType);
+    in.setState(state);
+    project(name).view("config").put(in, cb);
+  }
+
+  public static void getParent(Project.NameKey name,
+      final AsyncCallback<Project.NameKey> cb) {
+    project(name).view("parent").get(
+        new AsyncCallback<NativeString>() {
+          @Override
+          public void onSuccess(NativeString result) {
+            cb.onSuccess(new Project.NameKey(result.asString()));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            cb.onFailure(caught);
+          }
+        });
+  }
+
+  public static void getDescription(Project.NameKey name,
+      AsyncCallback<NativeString> cb) {
+    project(name).view("description").get(cb);
+  }
+
+  public static void setDescription(Project.NameKey name, String description,
+      AsyncCallback<NativeString> cb) {
+    RestApi call = project(name).view("description");
+    if (description != null && !description.isEmpty()) {
+      DescriptionInput input = DescriptionInput.create();
+      input.setDescription(description);
+      call.put(input, cb);
+    } else {
+      call.delete(cb);
+    }
+  }
+
+  public static RestApi project(Project.NameKey name) {
+    return new RestApi("/projects/").id(name.get());
   }
 
   private static class ProjectInput extends JavaScriptObject {
@@ -53,4 +147,77 @@
 
     final native void setCreateEmptyCommit(boolean cc) /*-{ if(cc)this.create_empty_commit=cc; }-*/;
   }
+
+  private static class ConfigInput extends JavaScriptObject {
+    static ConfigInput create() {
+      return (ConfigInput) createObject();
+    }
+
+    protected ConfigInput() {
+    }
+
+    final native void setDescription(String d)
+    /*-{ if(d)this.description=d; }-*/;
+
+    final void setUseContributorAgreements(InheritableBoolean v) {
+      setUseContributorAgreementsRaw(v.name());
+    }
+    private final native void setUseContributorAgreementsRaw(String v)
+    /*-{ if(v)this.use_contributor_agreements=v; }-*/;
+
+    final void setUseContentMerge(InheritableBoolean v) {
+      setUseContentMergeRaw(v.name());
+    }
+    private final native void setUseContentMergeRaw(String v)
+    /*-{ if(v)this.use_content_merge=v; }-*/;
+
+    final void setUseSignedOffBy(InheritableBoolean v) {
+      setUseSignedOffByRaw(v.name());
+    }
+    private final native void setUseSignedOffByRaw(String v)
+    /*-{ if(v)this.use_signed_off_by=v; }-*/;
+
+    final void setRequireChangeId(InheritableBoolean v) {
+      setRequireChangeIdRaw(v.name());
+    }
+    private final native void setRequireChangeIdRaw(String v)
+    /*-{ if(v)this.require_change_id=v; }-*/;
+
+    final native void setMaxObjectSizeLimit(String l)
+    /*-{ if(l)this.max_object_size_limit=l; }-*/;
+
+    final void setSubmitType(SubmitType t) {
+      setSubmitTypeRaw(t.name());
+    }
+    private final native void setSubmitTypeRaw(String t)
+    /*-{ if(t)this.submit_type=t; }-*/;
+
+    final void setState(Project.State s) {
+      setStateRaw(s.name());
+    }
+    private final native void setStateRaw(String s)
+    /*-{ if(s)this.state=s; }-*/;
+  }
+
+  private static class BranchInput extends JavaScriptObject {
+    static BranchInput create() {
+      return (BranchInput) createObject();
+    }
+
+    protected BranchInput() {
+    }
+
+    final native void setRevision(String r) /*-{ if(r)this.revision=r; }-*/;
+  }
+
+  private static class DescriptionInput extends JavaScriptObject {
+    static DescriptionInput create() {
+      return (DescriptionInput) createObject();
+    }
+
+    protected DescriptionInput() {
+    }
+
+    final native void setDescription(String d) /*-{ if(d)this.description=d; }-*/;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
index 67b6077..9a852a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.client.projects;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
index 0b1f905..073c949 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
@@ -17,15 +17,18 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
 
 /**
  * Class for grouping together callbacks and calling them in order.
  * <p>
  * Callbacks are added to the group with {@link #add(AsyncCallback)}, which
  * returns a wrapped callback suitable for passing to an asynchronous RPC call.
+ * The last callback must be added using {@link #addFinal(AsyncCallback)} or
+ * {@link #done()} must be invoked.
+ *
  * The enclosing group buffers returned results and ensures that
  * {@code onSuccess} is called exactly once for each callback in the group, in
  * the same order that callbacks were added. This allows callers to, for
@@ -39,82 +42,119 @@
  * processing it.
  */
 public class CallbackGroup {
-  private final List<Object> callbacks;
-  private final Map<Object, Object> results;
+  private final List<CallbackImpl<?>> callbacks;
+  private final Set<CallbackImpl<?>> remaining;
+  private boolean finalAdded;
+
   private boolean failed;
+  private Throwable failedThrowable;
+
+  public static <T> Callback<T> emptyCallback() {
+    return new Callback<T>() {
+      @Override
+      public void onSuccess(T result) {
+      }
+
+      @Override
+      public void onFailure(Throwable err) {
+      }
+    };
+  }
 
   public CallbackGroup() {
-    callbacks = new ArrayList<Object>();
-    results = new HashMap<Object, Object>();
+    callbacks = new ArrayList<CallbackImpl<?>>();
+    remaining = new HashSet<CallbackImpl<?>>();
   }
 
-  public <T> AsyncCallback<T> add(final AsyncCallback<T> cb) {
-    callbacks.add(cb);
-    return new AsyncCallback<T>() {
-      @Override
-      public void onSuccess(T result) {
-        results.put(cb, result);
-        CallbackGroup.this.onSuccess();
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-        CallbackGroup.this.onFailure(caught);
-      }
-    };
+  public <T> Callback<T> add(final AsyncCallback<T> cb) {
+    checkFinalAdded();
+    return handleAdd(cb);
   }
 
-  public <T> com.google.gwtjsonrpc.common.AsyncCallback<T> addGwtjsonrpc(
-      final com.google.gwtjsonrpc.common.AsyncCallback<T> cb) {
-    callbacks.add(cb);
-    return new com.google.gwtjsonrpc.common.AsyncCallback<T>() {
-      @Override
-      public void onSuccess(T result) {
-        results.put(cb, result);
-        CallbackGroup.this.onSuccess();
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-        CallbackGroup.this.onFailure(caught);
-      }
-    };
+  public <T> Callback<T> addFinal(final AsyncCallback<T> cb) {
+    checkFinalAdded();
+    finalAdded = true;
+    return handleAdd(cb);
   }
 
-  private void onSuccess() {
-    if (results.size() < callbacks.size()) {
-      return;
-    }
-    for (Object o : callbacks) {
-      Object result = results.get(o);
-      if (o instanceof AsyncCallback) {
-        @SuppressWarnings("unchecked")
-        AsyncCallback<Object> cb = (AsyncCallback<Object>) o;
-        cb.onSuccess(result);
-      } else {
-        @SuppressWarnings("unchecked")
-        com.google.gwtjsonrpc.common.AsyncCallback<Object> cb =
-            (com.google.gwtjsonrpc.common.AsyncCallback<Object>) o;
-        cb.onSuccess(result);
+  public void done() {
+    finalAdded = true;
+    applyAllSuccess();
+  }
+
+  private void applyAllSuccess() {
+    if (!failed && finalAdded && remaining.isEmpty()) {
+      for (CallbackImpl<?> cb : callbacks) {
+        cb.applySuccess();
       }
+      callbacks.clear();
     }
   }
 
-  private void onFailure(Throwable caught) {
+  private <T> Callback<T> handleAdd(AsyncCallback<T> cb) {
     if (failed) {
-      return;
+      cb.onFailure(failedThrowable);
+      return emptyCallback();
     }
-    failed = true;
-    for (Object o : callbacks) {
-      if (o instanceof AsyncCallback) {
-        @SuppressWarnings("unchecked")
-        AsyncCallback<Object> cb = (AsyncCallback<Object>) o;
-        cb.onFailure(caught);
-      } else {
-        @SuppressWarnings("unchecked")
-        com.google.gwtjsonrpc.common.AsyncCallback<Object> cb =
-            (com.google.gwtjsonrpc.common.AsyncCallback<Object>) o;
-        cb.onFailure(caught);
+
+    CallbackImpl<T> wrapper = new CallbackImpl<T>(cb);
+    callbacks.add(wrapper);
+    remaining.add(wrapper);
+    return wrapper;
+  }
+
+  private void checkFinalAdded() {
+    if (finalAdded) {
+      throw new IllegalStateException("final callback already added");
+    }
+  }
+
+  public interface Callback<T>
+      extends AsyncCallback<T>, com.google.gwtjsonrpc.common.AsyncCallback<T> {
+  }
+
+  private class CallbackImpl<T> implements Callback<T> {
+    AsyncCallback<T> delegate;
+    T result;
+
+    CallbackImpl(AsyncCallback<T> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void onSuccess(T value) {
+      if (failed) {
+        return;
+      }
+
+      this.result = value;
+      remaining.remove(this);
+      CallbackGroup.this.applyAllSuccess();
+    }
+
+    @Override
+    public void onFailure(Throwable caught) {
+      if (failed) {
+        return;
+      }
+
+      failed = true;
+      failedThrowable = caught;
+      for (CallbackImpl<?> cb : callbacks) {
+        cb.delegate.onFailure(failedThrowable);
+        cb.delegate = null;
+        cb.result = null;
+      }
+      callbacks.clear();
+      remaining.clear();
+    }
+
+    void applySuccess() {
+      AsyncCallback<T> cb = delegate;
+      if (cb != null) {
+        delegate = null;
+        cb.onSuccess(result);
+        result = null;
       }
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
index d56eeaf..2e407d4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -22,6 +22,10 @@
 
 /** A map of native JSON objects, keyed by a string. */
 public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
+  public static <T extends JavaScriptObject> NativeMap<T> create() {
+    return createObject().cast();
+  }
+
   /**
    * Loop through the result map's entries and copy the key strings into the
    * "name" property of the corresponding child object. This only runs on the
@@ -80,6 +84,7 @@
   }
 
   public final native T get(String n) /*-{ return this[n]; }-*/;
+  public final native void put(String n, T v) /*-{ this[n] = v; }-*/;
 
   public final native void copyKeysIntoChildren(String p)
   /*-{
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
index 573c5e7..be4cfd6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
@@ -19,12 +19,18 @@
 
 /** Wraps a String that was returned from a JSON API. */
 public final class NativeString extends JavaScriptObject {
-  static NativeString wrap(String value) {
-    NativeString ns = (NativeString) createObject();
-    ns.set(value);
-    return ns;
+  private static final JavaScriptObject TYPE = init();
+
+  private static final native JavaScriptObject init()
+  /*-{ return function(s){this.s=s} }-*/;
+
+  static final NativeString wrap(String s) {
+    return wrap0(TYPE, s);
   }
 
+  private static final native NativeString wrap0(JavaScriptObject T, String s)
+  /*-{ return new T(s) }-*/;
+
   public final native String asString() /*-{ return this.s; }-*/;
   private final native void set(String v) /*-{ this.s = v; }-*/;
 
@@ -43,6 +49,13 @@
     };
   }
 
+  public static final boolean is(JavaScriptObject o) {
+    return is(TYPE, o);
+  }
+
+  private static final native boolean is(JavaScriptObject T, JavaScriptObject o)
+  /*-{ return o instanceof T }-*/;
+
   protected NativeString() {
   }
 }
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 4ee63c6..4666d34 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
@@ -93,6 +93,7 @@
       case 405: // Method Not Allowed
       case 409: // Conflict
       case 412: // Precondition Failed
+      case 422: // Unprocessable Entity
       case 429: // Too Many Requests (RFC 6585)
         return true;
 
@@ -105,9 +106,11 @@
 
   private static class HttpCallback<T extends JavaScriptObject>
       implements RequestCallback {
+    private final boolean background;
     private final AsyncCallback<T> cb;
 
-    HttpCallback(AsyncCallback<T> cb) {
+    HttpCallback(boolean bg, AsyncCallback<T> cb) {
+      this.background = bg;
       this.cb = cb;
     }
 
@@ -116,11 +119,15 @@
       int status = res.getStatusCode();
       if (status == Response.SC_NO_CONTENT) {
         cb.onSuccess(null);
-        RpcStatus.INSTANCE.onRpcComplete();
+        if (!background) {
+          RpcStatus.INSTANCE.onRpcComplete();
+        }
 
       } else if (200 <= status && status < 300) {
         if (!isJsonBody(res)) {
-          RpcStatus.INSTANCE.onRpcComplete();
+          if (!background) {
+            RpcStatus.INSTANCE.onRpcComplete();
+          }
           cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE, "Expected "
               + JSON_TYPE + "; received Content-Type: "
               + res.getHeader("Content-Type")));
@@ -132,14 +139,18 @@
           // javac generics bug
           data = RestApi.<T>cast(parseJson(res));
         } catch (JSONException e) {
-          RpcStatus.INSTANCE.onRpcComplete();
+          if (!background) {
+            RpcStatus.INSTANCE.onRpcComplete();
+          }
           cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE,
               "Invalid JSON: " + e.getMessage()));
           return;
         }
 
         cb.onSuccess(data);
-        RpcStatus.INSTANCE.onRpcComplete();
+        if (!background) {
+          RpcStatus.INSTANCE.onRpcComplete();
+        }
 
       } else {
         String msg;
@@ -161,14 +172,18 @@
           msg = res.getStatusText();
         }
 
-        RpcStatus.INSTANCE.onRpcComplete();
+        if (!background) {
+          RpcStatus.INSTANCE.onRpcComplete();
+        }
         cb.onFailure(new StatusCodeException(status, msg));
       }
     }
 
     @Override
     public void onError(Request req, Throwable err) {
-      RpcStatus.INSTANCE.onRpcComplete();
+      if (!background) {
+        RpcStatus.INSTANCE.onRpcComplete();
+      }
       if (err.getMessage().contains("XmlHttpRequest.status")) {
         cb.onFailure(new StatusCodeException(
             SC_UNAVAILABLE,
@@ -181,6 +196,7 @@
 
   private StringBuilder url;
   private boolean hasQueryParams;
+  private boolean background;
   private String ifNoneMatch;
 
   /**
@@ -275,6 +291,11 @@
     return this;
   }
 
+  public RestApi background() {
+    background = true;
+    return this;
+  }
+
   public String url() {
     return url.toString();
   }
@@ -289,9 +310,11 @@
 
   private <T extends JavaScriptObject> void send(
       Method method, AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<T>(cb);
+    HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
     try {
-      RpcStatus.INSTANCE.onRpcStart();
+      if (!background) {
+        RpcStatus.INSTANCE.onRpcStart();
+      }
       request(method).sendRequest(null, httpCallback);
     } catch (RequestException e) {
       httpCallback.onError(null, e);
@@ -304,6 +327,11 @@
     sendJSON(POST, content, cb);
   }
 
+  public <T extends JavaScriptObject> void post(String content,
+      AsyncCallback<T> cb) {
+    sendRaw(POST, content, cb);
+  }
+
   public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
     send(PUT, cb);
   }
@@ -317,9 +345,11 @@
   private <T extends JavaScriptObject> void sendJSON(
       Method method, JavaScriptObject content,
       AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<T>(cb);
+    HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
     try {
-      RpcStatus.INSTANCE.onRpcStart();
+      if (!background) {
+        RpcStatus.INSTANCE.onRpcStart();
+      }
       String body = new JSONObject(content).toString();
       RequestBuilder req = request(method);
       req.setHeader("Content-Type", JSON_UTF8);
@@ -329,6 +359,21 @@
     }
   }
 
+  private <T extends JavaScriptObject> void sendRaw(Method method, String body,
+      AsyncCallback<T> cb) {
+    HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
+    try {
+      if (!background) {
+        RpcStatus.INSTANCE.onRpcStart();
+      }
+      RequestBuilder req = request(method);
+      req.setHeader("Content-Type", TEXT_TYPE);
+      req.sendRequest(body, httpCallback);
+    } catch (RequestException e) {
+      httpCallback.onError(null, e);
+    }
+  }
+
   private RequestBuilder request(Method method) {
     RequestBuilder req = new RequestBuilder(method, url());
     if (ifNoneMatch != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index ef52176..7f11df7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -65,7 +65,7 @@
     add(l);
   }
 
-  private static String owner(AccountInfo ai) {
+  public static String owner(AccountInfo ai) {
     if (ai.email() != null) {
       return ai.email();
     } else if (ai.name() != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java
new file mode 100644
index 0000000..295842c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeDetailCache;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gwt.user.client.ui.FocusWidget;
+
+public abstract class ActionDialog extends CommentedActionDialog<ChangeDetail> {
+  public ActionDialog(final FocusWidget enableOnFailure, final boolean redirect,
+      String dialogTitle, String dialogHeading) {
+    super(dialogTitle, dialogHeading, new ChangeDetailCache.IgnoreErrorCallback() {
+        @Override
+        public void onSuccess(ChangeDetail result) {
+          if (redirect) {
+            Gerrit.display(PageLinks.toChange(result.getChange().getId()));
+          } else {
+            super.onSuccess(result);
+          }
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          enableOnFailure.setEnabled(true);
+        }
+      });
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
index fddae84..c9a0590 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
@@ -57,7 +57,7 @@
     }
   }
 
-  private static String query(Project.NameKey project, Change.Status status,
+  public static String query(Project.NameKey project, Change.Status status,
       String branch, String topic) {
     String query = PageLinks.projectQuery(project, status);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
index d708286..72c80ac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
@@ -15,13 +15,11 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.user.client.DOM;
 
 public class ChangeLink extends InlineHyperlink {
   public static String permalink(final Change.Id c) {
@@ -33,7 +31,7 @@
 
   public ChangeLink(final String text, final Change.Id c) {
     super(text, PageLinks.toChange(c));
-    DOM.setElementProperty(getElement(), "href", permalink(c));
+    getElement().setPropertyString("href", permalink(c));
     cid = c;
     psid = null;
   }
@@ -57,14 +55,6 @@
 
   @Override
   public void go() {
-    Gerrit.display(getTargetHistoryToken(), createScreen());
-  }
-
-  private Screen createScreen() {
-    if (psid != null) {
-      return new ChangeScreen(psid);
-    } else {
-      return new ChangeScreen(cid);
-    }
+    Gerrit.display(getTargetHistoryToken());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
new file mode 100644
index 0000000..3ffd25f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.projects.BranchInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public abstract class CherryPickDialog extends ActionDialog {
+  private SuggestBox newBranch;
+  private List<BranchInfo> branches;
+
+  public CherryPickDialog(final FocusWidget enableOnFailure, Project.NameKey project) {
+    super(enableOnFailure, true, Util.C.cherryPickTitle(), Util.C
+        .cherryPickCommitMessage());
+    ProjectApi.getBranches(project,
+        new GerritCallback<JsArray<BranchInfo>>() {
+          @Override
+          public void onSuccess(JsArray<BranchInfo> result) {
+            branches = Natives.asList(result);
+          }
+        });
+
+    newBranch = new SuggestBox(new HighlightSuggestOracle() {
+      @Override
+      protected void onRequestSuggestions(Request request, Callback done) {
+        LinkedList<BranchSuggestion> suggestions =
+            new LinkedList<BranchSuggestion>();
+        for (final BranchInfo b : branches) {
+          if (b.ref().indexOf(request.getQuery()) >= 0) {
+            suggestions.add(new BranchSuggestion(b));
+          }
+        }
+        done.onSuggestionsReady(request, new Response(suggestions));
+      }
+    });
+
+    newBranch.setWidth("100%");
+    newBranch.getElement().getStyle().setProperty("boxSizing", "border-box");
+    message.setCharacterWidth(70);
+
+    final FlowPanel mwrap = new FlowPanel();
+    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    mwrap.add(newBranch);
+
+    panel.insert(mwrap, 0);
+    panel.insert(new SmallHeading(Util.C.headingCherryPickBranch()), 0);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    newBranch.setFocus(true);
+  }
+
+  public String getDestinationBranch() {
+    return newBranch.getText();
+  }
+
+  class BranchSuggestion implements Suggestion {
+    private BranchInfo branch;
+
+    public BranchSuggestion(BranchInfo branch) {
+      this.branch = branch;
+    }
+
+    @Override
+    public String getDisplayString() {
+      return branch.ref();
+    }
+
+    @Override
+    public String getReplacementString() {
+      return branch.getShortName();
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 05c6b5a..bb50b19 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -50,6 +50,7 @@
     HasFocusHandlers, FocusHandler, HasBlurHandlers, BlurHandler {
   private static final int SUMMARY_LENGTH = 75;
   private final HandlerManager handlerManager = new HandlerManager(this);
+  private final FlowPanel body;
   private final FlexTable header;
   private final InlineLabel messageSummary;
   private final FlowPanel content;
@@ -73,7 +74,7 @@
 
   protected CommentPanel(CommentLinkProcessor commentLinkProcessor) {
     this.commentLinkProcessor = commentLinkProcessor;
-    final FlowPanel body = new FlowPanel();
+    body = new FlowPanel();
     initWidget(body);
     setStyleName(Gerrit.RESOURCES.css().commentPanel());
 
@@ -127,8 +128,10 @@
   }
 
   public void setAuthorNameText(final AccountInfo author, final String nameText) {
-    header.setWidget(0, 0, new AvatarImage(author, 26));
+    header.setWidget(0, 0, new AvatarImage(author));
     header.setText(0, 1, nameText);
+    body.getElement().setAttribute("email", author.email());
+    body.getElement().setAttribute("name", author.name());
   }
 
   protected void setDateText(final String dateText) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
index 263703e..b8fa373 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
@@ -21,7 +21,6 @@
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.DOM;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -55,7 +54,7 @@
     message = new NpTextArea();
     message.setCharacterWidth(60);
     message.setVisibleLines(10);
-    DOM.setElementPropertyBoolean(message.getElement(), "spellcheck", true);
+    message.getElement().setPropertyBoolean("spellcheck", true);
     setFocusOn(message);
     sendButton = new Button(Util.C.commentedActionButtonSend());
     sendButton.addClickHandler(new ClickHandler() {
@@ -67,7 +66,7 @@
     });
 
     cancelButton = new Button(Util.C.commentedActionButtonCancel());
-    DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px");
+    cancelButton.getElement().getStyle().setProperty("float", "right");
     cancelButton.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
@@ -82,6 +81,7 @@
     buttonPanel = new FlowPanel();
     buttonPanel.add(sendButton);
     buttonPanel.add(cancelButton);
+    buttonPanel.getElement().getStyle().setProperty("marginTop", "4px");
 
     panel = new FlowPanel();
     panel.add(new SmallHeading(heading));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
index c93969d..b5bfd15 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
@@ -47,15 +47,15 @@
       final Element tr1 = DOM.getChild(tbody, 0);
       final Element tr2 = DOM.getChild(tbody, 1);
 
-      DOM.setElementProperty(DOM.getChild(tr1, 0), "width", "20px");
-      DOM.setElementPropertyInt(DOM.getChild(tr2, 0), "colSpan", 2);
+      DOM.getChild(tr1, 0).setPropertyString("width", "20px");
+      DOM.getChild(tr2, 0).setPropertyInt("colSpan", 2);
       headerParent = tr1;
     }
 
     header = new ComplexPanel() {
       {
         setElement(DOM.createTD());
-        DOM.setInnerHTML(getElement(), "&nbsp;");
+        getElement().setInnerHTML("&nbsp;");
         addStyleName(Gerrit.RESOURCES.css().complexHeader());
       }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
index 1edb8fd..0793527 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
@@ -25,6 +25,7 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 
+import java.util.Comparator;
 import java.util.Iterator;
 
 public abstract class FancyFlexTable<RowItem> extends Composite {
@@ -58,6 +59,71 @@
     setRowItem(table.getCellFormatter().getElement(row, 0), item);
   }
 
+  /**
+   * Finds an item in the table.
+   *
+   * @param comparator comparator by which the items in the table are sorted
+   * @param item the item that should be found
+   * @return if the item is found the number of the row that contains the item;
+   *         if the item is not found <code>-1</code>
+   */
+  protected int findRowItem(Comparator<RowItem> comparator, RowItem item) {
+    int row = lookupRowItem(comparator, item);
+    if (row < table.getRowCount()
+        && comparator.compare(item, getRowItem(row)) == 0) {
+      return row;
+    }
+    return -1;
+  }
+
+  /**
+   * Finds the number of the row where a new item should be inserted into the
+   * table.
+   *
+   * @param comparator comparator by which the items in the table are sorted
+   * @param item the new item that should be inserted
+   * @return if the item is not yet contained in the table, the number of the
+   *         row where the new item should be inserted; if the item is already
+   *         contained in the table <code>-1</code>
+   */
+  protected int getInsertRow(Comparator<RowItem> comparator, RowItem item) {
+    int row = lookupRowItem(comparator, item);
+    if (row >= table.getRowCount()
+        || comparator.compare(item, getRowItem(row)) != 0) {
+      return row;
+    }
+    return -1;
+  }
+
+  /**
+   * Makes a binary search for the given row item over the table.
+   *
+   * @param comparator comparator by which the items in the table are sorted
+   * @param item the item that should be looked up
+   * @return if the item is found the number of the row that contains the item;
+   *         if the item is not found the number of the row where the item
+   *         should be inserted according to the given comparator.
+   */
+  private int lookupRowItem(Comparator<RowItem> comparator, RowItem item) {
+    int left = 1;
+    int right = table.getRowCount() - 1;
+    while (left <= right) {
+      int middle = (left + right) >>> 1; // (left+right)/2
+      RowItem i = getRowItem(middle);
+      int cmp = comparator.compare(i, item);
+
+      if (cmp < 0) {
+        left = middle + 1;
+      } else if (cmp > 0) {
+        right = middle - 1;
+      } else {
+        // item is already contained in the table
+        return middle;
+      }
+    }
+    return left;
+  }
+
   protected void resetHtml(final SafeHtml body) {
     for (final Iterator<Widget> i = table.iterator(); i.hasNext();) {
       i.next();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
index c72969e..2836e0f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.ui.FancyFlexTable.MyFlexTable;
 import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.HTMLTable;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 
 public class FancyFlexTableImpl {
-  public void resetHtml(final MyFlexTable myTable, final SafeHtml body) {
-    SafeHtml.set(getBodyElement(myTable), body);
+  public void resetHtml(final FlexTable myTable, final SafeHtml body) {
+    SafeHtml.setInnerHTML(getBodyElement(myTable), body);
   }
 
   protected static native Element getBodyElement(HTMLTable myTable)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
index 17e8ddd..c85bb0e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
@@ -14,22 +14,22 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.ui.FancyFlexTable.MyFlexTable;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.HTMLTable;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 public class FancyFlexTableImplIE6 extends FancyFlexTableImpl {
   @Override
-  public void resetHtml(final MyFlexTable myTable, final SafeHtml bodyHtml) {
+  public void resetHtml(final FlexTable myTable, final SafeHtml bodyHtml) {
     final Element oldBody = getBodyElement(myTable);
     final Element newBody = parseBody(bodyHtml);
     assert newBody != null;
 
     final Element tableElem = DOM.getParent(oldBody);
-    DOM.removeChild(tableElem, oldBody);
+    tableElem.removeChild(oldBody);
     setBodyElement(myTable, newBody);
     DOM.appendChild(tableElem, newBody);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
index 1261b42..fc1661a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
@@ -55,7 +55,7 @@
   @Override
   public void onBrowserEvent(final Event event) {
     if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) {
-      DOM.eventPreventDefault(event);
+      event.preventDefault();
       go();
     } else {
       super.onBrowserEvent(event);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
index 8f64887..9d41774 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
@@ -33,10 +33,14 @@
     super(text, token);
   }
 
+  /** Creates an empty link. */
+  public InlineHyperlink() {
+  }
+
   @Override
   public void onBrowserEvent(final Event event) {
     if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) {
-      DOM.eventPreventDefault(event);
+      event.preventDefault();
       go();
     } else {
       super.onBrowserEvent(event);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
index fe7ffe9..bc88eed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
@@ -44,6 +44,10 @@
     add(i);
   }
 
+  public void insertItem(final LinkMenuItem i, int beforeIndex) {
+    insert(i, beforeIndex);
+  }
+
   public void clear() {
     body.clear();
   }
@@ -68,6 +72,18 @@
     body.add(i);
   }
 
+  public void insert(final Widget i, int beforeIndex) {
+    if (body.getWidgetCount() == 0 || body.getWidgetCount() <= beforeIndex) {
+      add(i);
+      return;
+    }
+    body.insert(i, beforeIndex);
+  }
+
+  public int getWidgetIndex(Widget i) {
+    return body.getWidgetIndex(i);
+  }
+
   public void onScreenLoad(ScreenLoadEvent event) {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index 63c2636..788c977 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -22,6 +22,7 @@
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ScrollPanel;
@@ -195,11 +196,12 @@
       final Element tr = DOM.getParent(fmt.getElement(currentRow, C_ARROW));
       UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), false);
     }
-    if (newRow >= 0) {
+    if (0 <= newRow && newRow < table.getRowCount()
+        && getRowItem(newRow) != null) {
       table.setWidget(newRow, C_ARROW, pointer);
       final Element tr = DOM.getParent(fmt.getElement(newRow, C_ARROW));
       UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), true);
-      if (scroll) {
+      if (scroll && isAttached()) {
         scrollIntoView(tr);
       }
     } else if (clear) {
@@ -230,7 +232,38 @@
         }
       });
     } else {
-      tr.scrollIntoView();
+      int rt = tr.getAbsoluteTop();
+      int rl = tr.getAbsoluteLeft();
+      int rb = tr.getAbsoluteBottom();
+
+      int wt = Window.getScrollTop();
+      int wl = Window.getScrollLeft();
+
+      int wh = Window.getClientHeight();
+      int ww = Window.getClientWidth();
+      int wb = wt + wh;
+
+      // If the row is partially or fully obscured, scroll:
+      //
+      // rl < wl: Row left edge is off screen to left.
+      // rt < wt: Row top is above top of window.
+      // wb < rt: Row top is below bottom of window.
+      // wb < rb: Row bottom is below bottom of window.
+      if (rl < wl || rt < wt || wb < rt || wb < rb) {
+        if (rl < wl) {
+          // Left edge needs to move to make it visible.
+          // If the row fully fits in the window, set 0.
+          if (tr.getAbsoluteRight() < ww) {
+            wl = 0;
+          } else {
+            wl = Math.max(tr.getAbsoluteLeft() - 5, 0);
+          }
+        }
+
+        // Vertically center the row in the window.
+        int h = (wh - (rb - rt)) / 2;
+        Window.scrollTo(wl, Math.max(rt - h, 0));
+      }
     }
   }
 
@@ -255,12 +288,15 @@
   }
 
   @Override
-  protected void resetHtml(SafeHtml body) {
+  public void resetHtml(SafeHtml body) {
     currentRow = -1;
     super.resetHtml(body);
   }
 
   public void finishDisplay() {
+    if (currentRow >= table.getRowCount()) {
+      currentRow = -1;
+    }
     if (saveId != null) {
       movePointerTo(savedPositions.get(saveId));
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
index a7d49a4..8082527 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -47,6 +47,7 @@
 
   private final FocusWidget widget;
   private Map<TextBoxBase, String> strings = new HashMap<TextBoxBase, String>();
+  private String originalValue;
 
 
   // The first parameter to the contructors must be the FocusWidget to enable,
@@ -54,6 +55,7 @@
 
   public OnEditEnabler(final FocusWidget w, final TextBoxBase tb) {
     this(w);
+    originalValue = tb.getValue().trim();
     listenTo(tb);
   }
 
@@ -75,7 +77,7 @@
   // Register input widgets to be listened to
 
   public void listenTo(final TextBoxBase tb) {
-    strings.put(tb, tb.getText());
+    strings.put(tb, tb.getText().trim());
     tb.addKeyPressHandler(this);
 
     // Is there another way to capture middle button X11 pastes in browsers
@@ -89,7 +91,7 @@
     tb.addFocusHandler(new FocusHandler() {
         @Override
         public void onFocus(FocusEvent event) {
-          strings.put(tb, tb.getText());
+          strings.put(tb, tb.getText().trim());
         }
       });
 
@@ -145,7 +147,7 @@
         Scheduler.get().scheduleDeferred(new ScheduledCommand() {
           @Override
           public void execute() {
-            if (box.getValue().trim().length() == 0) {
+            if (box.getValue().trim().equals(originalValue)) {
               widget.setEnabled(false);
             }
           }
@@ -173,7 +175,7 @@
         if (orig == null) {
           orig = "";
         }
-        if (! orig.equals(tb.getText())) {
+        if (! orig.equals(tb.getText().trim())) {
           widget.setEnabled(true);
         }
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java
index 0ae29bf..dc2c73d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java
@@ -24,7 +24,7 @@
 public class ProjectSearchLink extends InlineHyperlink {
 
   public ProjectSearchLink(Project.NameKey projectName) {
-    super(" ", PageLinks.toProjectDashboard(projectName, "default"));
+    super(" ", PageLinks.toProjectDefaultDashboard(projectName));
     setTitle(Util.C.projectListQueryLink());
     final Image image = new Image(Gerrit.RESOURCES.queryIcon());
     DOM.insertBefore(getElement(), image.getElement(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index a3a5052..052878b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -76,6 +76,7 @@
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().projectNameColumn());
     fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
 
     populate(row, k);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index a26db05..0cabdda 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -48,6 +48,7 @@
   protected Screen() {
     initWidget(new FlowPanel());
     setStyleName(Gerrit.RESOURCES.css().screen());
+    body = new FlowPanel();
   }
 
   @Override
@@ -76,7 +77,7 @@
   protected void onInitUI() {
     final FlowPanel me = (FlowPanel) getWidget();
     me.add(header = new Grid(1, Cols.values().length));
-    me.add(body = new FlowPanel());
+    me.add(body);
 
     headerText = new InlineLabel();
     if (titleWidget == null) {
@@ -107,7 +108,7 @@
       headerText.setText(text);
       header.setVisible(true);
     }
-    if (windowTitle == null || windowTitle == old) {
+    if (windowTitle == null || windowTitle.equals(old)) {
       setWindowTitle(text);
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java
new file mode 100644
index 0000000..6e754a2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.user.client.History;
+import com.google.gwtexpui.globalkey.client.DocWidget;
+
+/** Checks for user keyboard and mouse activity. */
+public class UserActivityMonitor {
+  private static final long TIMEOUT = 10 * 60 * 1000;
+  private static final MonitorImpl impl;
+
+  /**
+   * @return true if there has been keyboard and/or mouse activity in recent
+   *         enough history to believe a user is still controlling this session.
+   */
+  public static boolean isActive() {
+    return impl.active || impl.recent;
+  }
+
+  public static HandlerRegistration addValueChangeHandler(
+      ValueChangeHandler<Boolean> handler) {
+    return impl.addValueChangeHandler(handler);
+  }
+
+  static {
+    impl = new MonitorImpl();
+    DocWidget.get().addKeyPressHandler(impl);
+    DocWidget.get().addMouseMoveHandler(impl);
+    History.addValueChangeHandler(impl);
+    Scheduler.get().scheduleFixedDelay(impl, 60 * 1000);
+  }
+
+  private UserActivityMonitor() {
+  }
+
+  private static class MonitorImpl implements RepeatingCommand,
+      KeyPressHandler, MouseMoveHandler, ValueChangeHandler<String>,
+      HasValueChangeHandlers<Boolean> {
+    private final EventBus bus = new SimpleEventBus();
+    private boolean recent = true;
+    private boolean active = true;
+    private long last = System.currentTimeMillis();
+
+    @Override
+    public void onKeyPress(KeyPressEvent event) {
+      recent = true;
+    }
+
+    @Override
+    public void onMouseMove(MouseMoveEvent event) {
+      recent = true;
+    }
+
+    @Override
+    public void onValueChange(ValueChangeEvent<String> event) {
+      recent = true;
+    }
+
+    @Override
+    public boolean execute() {
+      long now = System.currentTimeMillis();
+      if (recent) {
+        if (!active) {
+          ValueChangeEvent.fire(this, active);
+        }
+        recent = false;
+        active = true;
+        last = now;
+      } else if (active && (now - last) > TIMEOUT) {
+        active = false;
+        ValueChangeEvent.fire(this, false);
+      }
+      return true;
+    }
+
+    @Override
+    public HandlerRegistration addValueChangeHandler(
+        ValueChangeHandler<Boolean> handler) {
+      return bus.addHandler(ValueChangeEvent.getType(), handler);
+    }
+
+    @Override
+    public void fireEvent(GwtEvent<?> event) {
+      bus.fireEvent(event);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
new file mode 100644
index 0000000..24a0f57
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
@@ -0,0 +1,23 @@
+<!--
+ 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.
+-->
+<module>
+  <inherits name='com.google.gwt.logging.Logging'/>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <source path='addon'/>
+  <source path='lib'/>
+  <source path='keymap'/>
+  <source path='mode'/>
+</module>
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
new file mode 100644
index 0000000..c5a047c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -0,0 +1,338 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import net.codemirror.lib.TextMarker.FromTo;
+
+/**
+ * Glue to connect CodeMirror to be callable from GWT.
+ *
+ * @see <a href="http://codemirror.net/doc/manual.html#api">CodeMirror API</a>
+ */
+public class CodeMirror extends JavaScriptObject {
+  public static void initLibrary(AsyncCallback<Void> cb) {
+    Loader.initLibrary(cb);
+  }
+
+  public static native CodeMirror create(Element parent,
+      Configuration cfg) /*-{
+    return $wnd.CodeMirror(parent, cfg);
+  }-*/;
+
+  public final native void setOption(String option, boolean value) /*-{
+    this.setOption(option, value);
+  }-*/;
+
+  public final native void setOption(String option, double value) /*-{
+    this.setOption(option, value);
+  }-*/;
+
+  public final native void setOption(String option, JavaScriptObject val) /*-{
+    this.setOption(option, val);
+  }-*/;
+
+  public final native void setOptionToInfinity(String option) /*-{
+    this.setOption(option, Infinity);
+  }-*/;
+
+  public final native void setValue(String v) /*-{ this.setValue(v); }-*/;
+
+  public final native void setWidth(double w) /*-{ this.setSize(w, null); }-*/;
+  public final native void setWidth(String w) /*-{ this.setSize(w, null); }-*/;
+  public final native void setHeight(double h) /*-{ this.setSize(null, h); }-*/;
+  public final native void setHeight(String h) /*-{ this.setSize(null, h); }-*/;
+
+  public final native void refresh() /*-{ this.refresh(); }-*/;
+  public final native Element getWrapperElement() /*-{ return this.getWrapperElement(); }-*/;
+
+  public final native TextMarker markText(LineCharacter from, LineCharacter to,
+      Configuration options) /*-{
+    return this.markText(from, to, options);
+  }-*/;
+
+  public enum LineClassWhere {
+    TEXT, BACKGROUND, WRAP;
+  }
+
+  public final void addLineClass(int line, LineClassWhere where,
+      String className) {
+    addLineClassNative(line, where.name().toLowerCase(), className);
+  }
+
+  private final native void addLineClassNative(int line, String where,
+      String lineClass) /*-{
+    this.addLineClass(line, where, lineClass);
+  }-*/;
+
+  public final void addLineClass(LineHandle line, LineClassWhere where,
+      String className) {
+    addLineClassNative(line, where.name().toLowerCase(), className);
+  }
+
+  private final native void addLineClassNative(LineHandle line, String where,
+      String lineClass) /*-{
+    this.addLineClass(line, where, lineClass);
+  }-*/;
+
+  public final void removeLineClass(int line, LineClassWhere where,
+      String className) {
+    removeLineClassNative(line, where.name().toLowerCase(), className);
+  }
+
+  private final native void removeLineClassNative(int line, String where,
+      String lineClass) /*-{
+    this.removeLineClass(line, where, lineClass);
+  }-*/;
+
+  public final void removeLineClass(LineHandle line, LineClassWhere where,
+      String className) {
+    removeLineClassNative(line, where.name().toLowerCase(), className);
+  }
+
+  private final native void removeLineClassNative(LineHandle line, String where,
+      String lineClass) /*-{
+    this.removeLineClass(line, where, lineClass);
+  }-*/;
+
+
+  public final native void addWidget(LineCharacter pos, Element node,
+      boolean scrollIntoView) /*-{
+    this.addWidget(pos, node, scrollIntoView);
+  }-*/;
+
+  public final native LineWidget addLineWidget(int line, Element node,
+      Configuration options) /*-{
+    return this.addLineWidget(line, node, options);
+  }-*/;
+
+  public final native int lineAtHeight(double height) /*-{
+    return this.lineAtHeight(height);
+  }-*/;
+
+  public final native int lineAtHeight(double height, String mode) /*-{
+    return this.lineAtHeight(height, mode);
+  }-*/;
+
+  public final native double heightAtLine(int line) /*-{
+    return this.heightAtLine(line);
+  }-*/;
+
+  public final native double heightAtLine(int line, String mode) /*-{
+    return this.heightAtLine(line, mode);
+  }-*/;
+
+  public final native CodeMirrorDoc getDoc() /*-{
+    return this.getDoc();
+  }-*/;
+
+  public final native void scrollTo(double x, double y) /*-{
+    this.scrollTo(x, y);
+  }-*/;
+
+  public final native void scrollToY(double y) /*-{
+    this.scrollTo(null, y);
+  }-*/;
+
+  public final native ScrollInfo getScrollInfo() /*-{
+    return this.getScrollInfo();
+  }-*/;
+
+  public final native Viewport getViewport() /*-{
+    return this.getViewport();
+  }-*/;
+
+  public final native int getOldViewportSize() /*-{
+    return this.state.oldViewportSize || 0;
+  }-*/;
+
+  public final native void setOldViewportSize(int lines) /*-{
+    this.state.oldViewportSize = lines;
+  }-*/;
+
+  public final native void on(String event, Runnable thunk) /*-{
+    this.on(event, $entry(function() {
+      thunk.@java.lang.Runnable::run()();
+    }));
+  }-*/;
+
+  /** TODO: Break this line after updating GWT */
+  public final native void on(String event, EventHandler handler) /*-{
+    this.on(event, $entry(function(cm, e) {
+      handler.@net.codemirror.lib.CodeMirror.EventHandler::handle(Lnet/codemirror/lib/CodeMirror;Lcom/google/gwt/dom/client/NativeEvent;)(cm, e);
+    }));
+  }-*/;
+
+  public final native void on(String event, RenderLineHandler handler) /*-{
+    this.on(event, $entry(function(cm, h, ele) {
+      handler.@net.codemirror.lib.CodeMirror.RenderLineHandler::handle(Lnet/codemirror/lib/CodeMirror;Lnet/codemirror/lib/CodeMirror$LineHandle;Lcom/google/gwt/dom/client/Element;)(cm, h, ele);
+    }));
+  }-*/;
+
+  public final native void on(String event, GutterClickHandler handler) /*-{
+    this.on(event, $entry(function(cm, l, g, e) {
+      handler.@net.codemirror.lib.CodeMirror.GutterClickHandler::handle(Lnet/codemirror/lib/CodeMirror;ILjava/lang/String;Lcom/google/gwt/dom/client/NativeEvent;)(cm, l, g, e);
+    }));
+  }-*/;
+
+  public final native LineCharacter getCursor() /*-{
+    return this.getCursor();
+  }-*/;
+
+  public final native LineCharacter getCursor(String start) /*-{
+    return this.getCursor(start);
+  }-*/;
+
+  public final FromTo getSelectedRange() {
+    return FromTo.create(getCursor("start"), getCursor("end"));
+  };
+
+  public final native void setCursor(LineCharacter lineCh) /*-{
+    this.setCursor(lineCh);
+  }-*/;
+
+  public final native boolean somethingSelected() /*-{
+    return this.somethingSelected();
+  }-*/;
+
+  public final native boolean hasActiveLine() /*-{
+    return !!this.state.activeLine;
+  }-*/;
+
+  public final native LineHandle getActiveLine() /*-{
+    return this.state.activeLine;
+  }-*/;
+
+  public final native void setActiveLine(LineHandle line) /*-{
+    this.state.activeLine = line;
+  }-*/;
+
+  public final native void addKeyMap(KeyMap map) /*-{ this.addKeyMap(map); }-*/;
+
+  public final native void removeKeyMap(KeyMap map) /*-{ this.removeKeyMap(map); }-*/;
+
+  public final native void removeKeyMap(String name) /*-{ this.removeKeyMap(name); }-*/;
+
+  public static final native LineCharacter pos(int line, int ch) /*-{
+    return $wnd.CodeMirror.Pos(line, ch);
+  }-*/;
+
+  public static final native LineCharacter pos(int line) /*-{
+    return $wnd.CodeMirror.Pos(line);
+  }-*/;
+
+  public final native LineHandle getLineHandle(int line) /*-{
+    return this.getLineHandle(line);
+  }-*/;
+
+  public final native LineHandle getLineHandleVisualStart(int line) /*-{
+    return this.getLineHandleVisualStart(line);
+  }-*/;
+
+  public final native int getLineNumber(LineHandle handle) /*-{
+    return this.getLineNumber(handle);
+  }-*/;
+
+  public final native void focus() /*-{
+    this.focus();
+  }-*/;
+
+  public final native int lineCount() /*-{
+    return this.lineCount();
+  }-*/;
+
+  public final native Element getGutterElement() /*-{
+    return this.getGutterElement();
+  }-*/;
+
+  public final native Element getScrollerElement() /*-{
+    return this.getScrollerElement();
+  }-*/;
+
+  public final native Element getSizer() /*-{
+    return this.display.sizer;
+  }-*/;
+
+  public final native Element getInputField() /*-{
+    return this.getInputField();
+  }-*/;
+
+  public final native Element getScrollbarV() /*-{
+    return this.display.scrollbarV;
+  }-*/;
+
+  public static final native KeyMap cloneKeyMap(String name) /*-{
+    var i = $wnd.CodeMirror.keyMap[name];
+    var o = {};
+    for (n in i)
+      if (i.hasOwnProperty(n))
+        o[n] = i[n];
+    return o;
+  }-*/;
+
+  public final native void execCommand(String cmd) /*-{
+    this.execCommand(cmd);
+  }-*/;
+
+  public static final native void addKeyMap(String name, KeyMap km) /*-{
+    $wnd.CodeMirror.keyMap[name] = km;
+  }-*/;
+
+  public static final native void handleVimKey(CodeMirror cm, String key) /*-{
+    $wnd.CodeMirror.Vim.handleKey(cm, key);
+  }-*/;
+
+  public static final native void mapVimKey(String alias, String actual) /*-{
+    $wnd.CodeMirror.Vim.map(alias, actual);
+  }-*/;
+
+  public final native boolean hasVimSearchHighlight() /*-{
+    return this.state.vim && this.state.vim.searchState_ &&
+        !!this.state.vim.searchState_.getOverlay();
+  }-*/;
+
+  protected CodeMirror() {
+  }
+
+  public static class Viewport extends JavaScriptObject {
+    public final native int getFrom() /*-{ return this.from; }-*/;
+    public final native int getTo() /*-{ return this.to; }-*/;
+
+    protected Viewport() {
+    }
+  }
+
+  public static class LineHandle extends JavaScriptObject {
+    protected LineHandle(){
+    }
+  }
+
+  public interface EventHandler {
+    public void handle(CodeMirror instance, NativeEvent event);
+  }
+
+  public interface RenderLineHandler {
+    public void handle(CodeMirror instance, LineHandle handle, Element element);
+  }
+
+  public interface GutterClickHandler {
+    public void handle(CodeMirror instance, int line, String gutter,
+        NativeEvent clickEvent);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
new file mode 100644
index 0000000..ff5d230
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
@@ -0,0 +1,33 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** The Doc object representing the content in a CodeMirror */
+public class CodeMirrorDoc extends JavaScriptObject {
+
+  public final native void replaceRange(String replacement,
+      LineCharacter from, LineCharacter to) /*-{
+    this.replaceRange(replacement, from, to);
+  }-*/;
+
+  public final native void insertText(String insertion, LineCharacter at) /*-{
+    this.replaceRange(insertion, at);
+  }-*/;
+
+  protected CodeMirrorDoc() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
new file mode 100644
index 0000000..ab031eb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
@@ -0,0 +1,50 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Simple map-like structure to pass configuration to CodeMirror.
+ *
+ * @see <a href="http://codemirror.net/doc/manual.html#config">CodeMirror config</a>
+ * @see CodeMirror#create(com.google.gwt.dom.client.Element, Configuration)
+ */
+public class Configuration extends JavaScriptObject {
+  public static Configuration create() {
+    return createObject().cast();
+  }
+
+  public final native Configuration set(String name, String val)
+  /*-{ this[name] = val; return this; }-*/;
+
+  public final native Configuration set(String name, int val)
+  /*-{ this[name] = val; return this; }-*/;
+
+  public final native Configuration set(String name, double val)
+  /*-{ this[name] = val; return this; }-*/;
+
+  public final native Configuration set(String name, boolean val)
+  /*-{ this[name] = val; return this; }-*/;
+
+  public final native Configuration set(String name, JavaScriptObject val)
+  /*-{ this[name] = val; return this; }-*/;
+
+  public final native Configuration setInfinity(String name)
+  /*-{ this[name] = Infinity; return this; }-*/;
+
+  protected Configuration() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
new file mode 100644
index 0000000..83350c5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
@@ -0,0 +1,34 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** Object that associates a key or key combination with a handler. */
+public class KeyMap extends JavaScriptObject {
+  public static KeyMap create() {
+    return createObject().cast();
+  }
+
+  public final native KeyMap on(String key, Runnable thunk) /*-{
+    this[key] = function() { $entry(thunk.@java.lang.Runnable::run()()); };
+    return this;
+  }-*/;
+
+  public final native KeyMap remove(String key) /*-{ delete this[key]; }-*/;
+
+  protected KeyMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
new file mode 100644
index 0000000..bb60fe9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
@@ -0,0 +1,32 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+import com.google.gwt.resources.client.ExternalTextResource;
+
+interface Lib extends ClientBundle {
+  static final Lib I = GWT.create(Lib.class);
+
+  @Source("cm3.css")
+  ExternalTextResource css();
+
+  @Source("cm3.js")
+  @DoNotEmbed
+  DataResource js();
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
new file mode 100644
index 0000000..5076aff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
@@ -0,0 +1,44 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** {line, ch} objects used within CodeMirror. */
+public class LineCharacter extends JavaScriptObject {
+  public static LineCharacter create(int line, int ch) {
+    return createImpl(line, ch);
+  }
+
+  public static LineCharacter create(int line) {
+    return createImpl(line, 0);
+  }
+
+  private static LineCharacter createImpl(int line, int ch) {
+    LineCharacter lineCh = createObject().cast();
+    lineCh.setLine(line);
+    lineCh.setCh(ch);
+    return lineCh;
+  }
+
+  public final native void setLine(int line) /*-{ this.line = line; }-*/;
+  public final native void setCh(int ch) /*-{ this.ch = ch; }-*/;
+
+  public final native int getLine() /*-{ return this.line; }-*/;
+  public final native int getCh() /*-{ return this.ch; }-*/;
+
+  protected LineCharacter() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
new file mode 100644
index 0000000..c7b0300
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
@@ -0,0 +1,31 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** LineWidget objects used within CodeMirror. */
+public class LineWidget extends JavaScriptObject {
+  public static LineWidget create() {
+    return createObject().cast();
+  }
+
+  public final native void clear() /*-{ this.clear(); }-*/;
+  public final native void changed() /*-{ this.changed(); }-*/;
+  public final native JavaScriptObject getLine() /*-{ return this.line; }-*/;
+
+  protected LineWidget() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
new file mode 100644
index 0000000..d2954ba
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
@@ -0,0 +1,107 @@
+// 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 net.codemirror.lib;
+
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gwt.core.client.Callback;
+import com.google.gwt.core.client.ScriptInjector;
+import com.google.gwt.dom.client.ScriptElement;
+import com.google.gwt.dom.client.StyleInjector;
+import com.google.gwt.resources.client.ExternalTextResource;
+import com.google.gwt.resources.client.ResourceCallback;
+import com.google.gwt.resources.client.ResourceException;
+import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+class Loader {
+  private static native boolean isLibLoaded()
+  /*-{ return $wnd.hasOwnProperty('CodeMirror'); }-*/;
+
+  static void initLibrary(final AsyncCallback<Void> cb) {
+    if (isLibLoaded()) {
+      cb.onSuccess(null);
+    } else {
+      injectCss(Lib.I.css());
+      injectScript(Lib.I.js().getSafeUri(), new GerritCallback<Void>(){
+        @Override
+        public void onSuccess(Void result) {
+          initVimKeys();
+          cb.onSuccess(null);
+        }
+      });
+    }
+  }
+
+  private static void injectCss(ExternalTextResource css) {
+    try {
+      css.getText(new ResourceCallback<TextResource>() {
+        @Override
+        public void onSuccess(TextResource resource) {
+          StyleInjector.inject(resource.getText());
+        }
+
+        @Override
+        public void onError(ResourceException e) {
+          error(e);
+        }
+      });
+    } catch (ResourceException e) {
+      error(e);
+    }
+  }
+
+  static void injectScript(SafeUri js, final AsyncCallback<Void> callback) {
+    final ScriptElement[] script = new ScriptElement[1];
+    script[0] = ScriptInjector.fromUrl(js.asString())
+      .setWindow(ScriptInjector.TOP_WINDOW)
+      .setCallback(new Callback<Void, Exception>() {
+        @Override
+        public void onSuccess(Void result) {
+          script[0].removeFromParent();
+          callback.onSuccess(result);
+        }
+
+        @Override
+        public void onFailure(Exception reason) {
+          error(reason);
+          callback.onFailure(reason);
+        }
+       })
+      .inject()
+      .cast();
+  }
+
+  private static void initVimKeys() {
+    // TODO: Better custom keybindings, remove temporary navigation hacks.
+    KeyMap km = CodeMirror.cloneKeyMap("vim");
+    for (String s : new String[] {
+        "A", "C", "O", "R", "U", "Ctrl-C", "Ctrl-O"}) {
+      km.remove(s);
+    }
+    CodeMirror.addKeyMap("vim_ro", km);
+  }
+
+  private static void error(Exception e) {
+    Logger log = Logger.getLogger("net.codemirror");
+    log.log(Level.SEVERE, "Cannot load portions of CodeMirror", e);
+  }
+
+  private Loader() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
new file mode 100644
index 0000000..4c32f38
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
@@ -0,0 +1,180 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import net.codemirror.mode.Modes;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class ModeInjector {
+  /** Map of server content type to CodeMiror mode or content type. */
+  private static final Map<String, String> mimeAlias;
+
+  /** Map of content type "text/x-java" to mode name "clike". */
+  private static final Map<String, String> mimeModes;
+
+  /** Map of names such as "clike" to URI for code download. */
+  private static final Map<String, SafeUri> modeUris;
+
+  static {
+    DataResource[] all = {
+      Modes.I.clike(),
+      Modes.I.css(),
+      Modes.I.go(),
+      Modes.I.htmlmixed(),
+      Modes.I.javascript(),
+      Modes.I.properties(),
+      Modes.I.python(),
+      Modes.I.shell(),
+      Modes.I.sql(),
+      Modes.I.velocity(),
+      Modes.I.xml(),
+    };
+
+    mimeAlias = new HashMap<String, String>();
+    mimeModes = new HashMap<String, String>();
+    modeUris = new HashMap<String, SafeUri>();
+
+    for (DataResource m : all) {
+      modeUris.put(m.getName(), m.getSafeUri());
+    }
+    parseModeMap();
+  }
+
+  private static void parseModeMap() {
+    String mode = null;
+    for (String line : Modes.I.mode_map().getText().split("\n")) {
+      int eq = line.indexOf('=');
+      if (0 < eq) {
+        mimeAlias.put(
+          line.substring(0, eq).trim(),
+          line.substring(eq + 1).trim());
+      } else if (line.endsWith(":")) {
+        String n = line.substring(0, line.length() - 1);
+        if (modeUris.containsKey(n)) {
+          mode = n;
+        }
+      } else if (mode != null && line.contains("/")) {
+        mimeModes.put(line, mode);
+      } else {
+        mode = null;
+      }
+    }
+  }
+
+  public static String getContentType(String mode) {
+    String real = mode != null ? mimeAlias.get(mode) : null;
+    return real != null ? real : mode;
+  }
+
+  private static native boolean isModeLoaded(String n)
+  /*-{ return $wnd.CodeMirror.modes.hasOwnProperty(n); }-*/;
+
+  private static native boolean isMimeLoaded(String n)
+  /*-{ return $wnd.CodeMirror.mimeModes.hasOwnProperty(n); }-*/;
+
+  private static native JsArrayString getDependencies(String n)
+  /*-{ return $wnd.CodeMirror.modes[n].dependencies || []; }-*/;
+
+  private final Set<String> loading = new HashSet<String>(4);
+  private int pending;
+  private AsyncCallback<Void> appCallback;
+
+  public ModeInjector add(String name) {
+    if (name == null || isModeLoaded(name) || isMimeLoaded(name)) {
+      return this;
+    }
+
+    String mode = mimeModes.get(name);
+    if (mode == null) {
+      mode = name;
+    }
+
+    SafeUri uri = modeUris.get(mode);
+    if (uri == null) {
+      Logger.getLogger("net.codemirror").log(
+        Level.WARNING,
+        "CodeMirror mode " + mode + " not configured.");
+      return this;
+    }
+
+    loading.add(mode);
+    return this;
+  }
+
+  public void inject(AsyncCallback<Void> appCallback) {
+    this.appCallback = appCallback;
+    for (String mode : loading) {
+      beginLoading(mode);
+    }
+    if (pending == 0) {
+      appCallback.onSuccess(null);
+    }
+  }
+
+  private void beginLoading(final String mode) {
+    pending++;
+    Loader.injectScript(
+      modeUris.get(mode),
+      new AsyncCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          pending--;
+          ensureDependenciesAreLoaded(mode);
+          if (pending == 0) {
+            appCallback.onSuccess(null);
+          }
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          if (--pending == 0) {
+            appCallback.onFailure(caught);
+          }
+        }
+      });
+  }
+
+  private void ensureDependenciesAreLoaded(String mode) {
+    JsArrayString deps = getDependencies(mode);
+    for (int i = 0; i < deps.length(); i++) {
+      String d = deps.get(i);
+      if (loading.contains(d) || isModeLoaded(d)) {
+        continue;
+      }
+
+      SafeUri uri = modeUris.get(d);
+      if (uri == null) {
+        Logger.getLogger("net.codemirror").log(
+          Level.SEVERE,
+          "CodeMirror mode " + mode + " needs " + d);
+        continue;
+      }
+
+      loading.add(d);
+      beginLoading(d);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
new file mode 100644
index 0000000..0639416
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
@@ -0,0 +1,35 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** {left, top, width, height, clientWidth, clientHeight} objects returned by
+ * getScrollInfo(). */
+public class ScrollInfo extends JavaScriptObject {
+  public static ScrollInfo create() {
+    return createObject().cast();
+  }
+
+  public final native double getLeft() /*-{ return this.left; }-*/;
+  public final native double getTop() /*-{ return this.top; }-*/;
+  public final native double getWidth() /*-{ return this.width; }-*/;
+  public final native double getHeight() /*-{ return this.height; }-*/;
+  public final native double getClientWidth() /*-{ return this.clientWidth; }-*/;
+  public final native double getClientHeight() /*-{ return this.clientHeight; }-*/;
+
+  protected ScrollInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
new file mode 100644
index 0000000..f154e48
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
@@ -0,0 +1,56 @@
+// 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 net.codemirror.lib;
+
+import com.google.gerrit.client.diff.CommentRange;
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** Object that represents a text marker within CodeMirror */
+public class TextMarker extends JavaScriptObject {
+  public static TextMarker create() {
+    return createObject().cast();
+  }
+
+  public final native void clear() /*-{ this.clear(); }-*/;
+  public final native void changed() /*-{ this.changed(); }-*/;
+  public final native FromTo find() /*-{ return this.find(); }-*/;
+
+  protected TextMarker() {
+  }
+
+  public static class FromTo extends JavaScriptObject {
+    public static FromTo create(LineCharacter from, LineCharacter to) {
+      FromTo fromTo = createObject().cast();
+      fromTo.setFrom(from);
+      fromTo.setTo(to);
+      return fromTo;
+    }
+
+    public static FromTo create(CommentRange range) {
+      return create(
+          LineCharacter.create(range.start_line() - 1, range.start_character()),
+          LineCharacter.create(range.end_line() - 1, range.end_character()));
+    }
+
+    public final native LineCharacter getFrom() /*-{ return this.from; }-*/;
+    public final native LineCharacter getTo() /*-{ return this.to; }-*/;
+
+    public final native void setFrom(LineCharacter from) /*-{ this.from = from; }-*/;
+    public final native void setTo(LineCharacter to) /*-{ this.to = to; }-*/;
+
+    protected FromTo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
new file mode 100644
index 0000000..e2d3e3c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -0,0 +1,40 @@
+// 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 net.codemirror.mode;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+import com.google.gwt.resources.client.TextResource;
+
+public interface Modes extends ClientBundle {
+  public static final Modes I = GWT.create(Modes.class);
+
+  @Source("mode_map") TextResource mode_map();
+  @Source("clike/clike.js") @DoNotEmbed DataResource clike();
+  @Source("css/css.js") @DoNotEmbed DataResource css();
+  @Source("go/go.js") @DoNotEmbed DataResource go();
+  @Source("htmlmixed/htmlmixed.js") @DoNotEmbed DataResource htmlmixed();
+  @Source("javascript/javascript.js") @DoNotEmbed DataResource javascript();
+  @Source("properties/properties.js") @DoNotEmbed DataResource properties();
+  @Source("python/python.js") @DoNotEmbed DataResource python();
+  @Source("shell/shell.js") @DoNotEmbed DataResource shell();
+  @Source("sql/sql.js") @DoNotEmbed DataResource sql();
+  @Source("velocity/velocity.js") @DoNotEmbed DataResource velocity();
+  @Source("xml/xml.js") @DoNotEmbed DataResource xml();
+
+  // When adding a resource, update static initializer in ModeInjector.
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
new file mode 100644
index 0000000..e448fa9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
@@ -0,0 +1,55 @@
+clike:
+text/x-csrc
+text/x-c
+text/x-chdr
+text/x-c++src
+text/x-c++hdr
+text/x-java
+text/x-csharp
+text/x-scala
+
+css:
+text/css
+text/x-scss
+
+go:
+text/x-go
+
+htmlmixed:
+text/html
+
+javascript:
+text/javascript
+text/ecmascript
+application/javascript
+application/ecmascript
+application/json
+application/x-json
+text/typescript
+application/typescript
+
+properties:
+text/x-ini
+text/x-properties
+
+python:
+text/x-python
+
+shell:
+text/x-sh
+
+sql:
+text/x-sql
+text/x-mariadb
+text/x-mysql
+text/x-plsql
+
+velocity:
+text/velocity
+
+xml:
+text/xml
+application/xml
+
+application/x-javascript = application/javascript
+text/x-java-source = text/x-java
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java
new file mode 100644
index 0000000..12a616a
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+import com.googlecode.gwt.test.GwtModule;
+import com.googlecode.gwt.test.GwtTest;
+
+import net.codemirror.lib.LineCharacter;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/** Unit tests for EditIterator */
+@GwtModule("com.google.gerrit.GerritGwtUI")
+public class EditIteratorTest extends GwtTest {
+  private JsArrayString lines;
+
+  private void assertLineChsEqual(LineCharacter a, LineCharacter b) {
+    assertEquals(a.getLine() + "," + a.getCh(), b.getLine() + "," + b.getCh());
+  }
+
+  @Before
+  public void initialize() {
+    lines = (JsArrayString) JavaScriptObject.createArray();
+    lines.push("1st");
+    lines.push("2nd");
+    lines.push("3rd");
+  }
+
+  @Test
+  public void testNoAdvance() {
+    EditIterator iter = new EditIterator(lines, 0);
+    assertLineChsEqual(LineCharacter.create(0), iter.advance(0));
+  }
+
+  @Test
+  public void testSimpleAdvance() {
+    EditIterator iter = new EditIterator(lines, 0);
+    assertLineChsEqual(LineCharacter.create(0, 1), iter.advance(1));
+  }
+
+  @Test
+  public void testEndsBeforeNewline() {
+    EditIterator iter = new EditIterator(lines, 0);
+    assertLineChsEqual(LineCharacter.create(0, 3), iter.advance(3));
+  }
+
+  @Test
+  public void testEndsOnNewline() {
+    EditIterator iter = new EditIterator(lines, 0);
+    assertLineChsEqual(LineCharacter.create(1), iter.advance(4));
+  }
+
+  @Test
+  public void testAcrossNewline() {
+    EditIterator iter = new EditIterator(lines, 0);
+    assertLineChsEqual(LineCharacter.create(1, 1), iter.advance(5));
+  }
+
+  @Test
+  public void testContinueFromBeforeNewline() {
+    EditIterator iter = new EditIterator(lines, 0);
+    iter.advance(3);
+    assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(7));
+  }
+
+  @Test
+  public void testContinueFromAfterNewline() {
+    EditIterator iter = new EditIterator(lines, 0);
+    iter.advance(4);
+    assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(6));
+  }
+
+  @Test
+  public void testAcrossMultipleLines() {
+    EditIterator iter = new EditIterator(lines, 0);
+    assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(10));
+  }
+}
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
new file mode 100644
index 0000000..756e879
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+
+import org.junit.Test;
+
+/** Unit tests for LineMapper */
+public class LineMapperTest {
+
+  @Test
+  public void testAppendCommon() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendCommon(10);
+    assertEquals(10, mapper.getLineA());
+    assertEquals(10, mapper.getLineB());
+  }
+
+  @Test
+  public void testAppendInsert() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendInsert(10);
+    assertEquals(0, mapper.getLineA());
+    assertEquals(10, mapper.getLineB());
+  }
+
+  @Test
+  public void testAppendDelete() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendDelete(10);
+    assertEquals(10, mapper.getLineA());
+    assertEquals(0, mapper.getLineB());
+  }
+
+  @Test
+  public void testFindInCommon() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendCommon(10);
+    assertEquals(new LineOnOtherInfo(9, true),
+        mapper.lineOnOther(DisplaySide.A, 9));
+    assertEquals(new LineOnOtherInfo(9, true),
+        mapper.lineOnOther(DisplaySide.B, 9));
+  }
+
+  @Test
+  public void testFindAfterCommon() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendCommon(10);
+    assertEquals(new LineOnOtherInfo(10, true),
+        mapper.lineOnOther(DisplaySide.A, 10));
+    assertEquals(new LineOnOtherInfo(10, true),
+        mapper.lineOnOther(DisplaySide.B, 10));
+  }
+
+  @Test
+  public void testFindInInsertGap() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendInsert(10);
+    assertEquals(new LineOnOtherInfo(-1, false),
+        mapper.lineOnOther(DisplaySide.B, 9));
+  }
+
+  @Test
+  public void testFindAfterInsertGap() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendInsert(10);
+    assertEquals(new LineOnOtherInfo(0, true),
+        mapper.lineOnOther(DisplaySide.B, 10));
+    assertEquals(new LineOnOtherInfo(10, true),
+        mapper.lineOnOther(DisplaySide.A, 0));
+  }
+
+  @Test
+  public void testFindInDeleteGap() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendDelete(10);
+    assertEquals(new LineOnOtherInfo(-1, false),
+        mapper.lineOnOther(DisplaySide.A, 9));
+  }
+
+  @Test
+  public void testFindAfterDeleteGap() {
+    LineMapper mapper = new LineMapper();
+    mapper.appendDelete(10);
+    assertEquals(new LineOnOtherInfo(0, true),
+        mapper.lineOnOther(DisplaySide.A, 10));
+    assertEquals(new LineOnOtherInfo(10, true),
+        mapper.lineOnOther(DisplaySide.B, 0));
+  }
+}
diff --git a/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties b/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties
new file mode 100644
index 0000000..c0cbb30
--- /dev/null
+++ b/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties
@@ -0,0 +1 @@
+com.google.gerrit.GerritGwtUI = gwt-module
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
new file mode 100644
index 0000000..512e6e6
--- /dev/null
+++ b/gerrit-httpd/BUCK
@@ -0,0 +1,66 @@
+SRCS = glob(['src/main/java/**/*.java'])
+RESOURCES = glob(['src/main/resources/**/*'])
+
+java_library2(
+  name = 'httpd',
+  srcs = SRCS,
+  resources = RESOURCES,
+  deps = [
+    '//gerrit-antlr:query_exception',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-gwtexpui:linker_server',
+    '//gerrit-gwtexpui:server',
+    '//gerrit-patch-jgit:server',
+    '//gerrit-prettify:server',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-util-cli:cli',
+    '//lib:args4j',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+    '//lib:jsch',
+    '//lib:mime-util',
+    '//lib/commons:codec',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+    '//lib/jgit:jgit',
+    '//lib/jgit:jgit-servlet',
+    '//lib/log:api',
+  ],
+  compile_deps = ['//lib:servlet-api-3_0'],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'httpd-src',
+  srcs = SRCS + RESOURCES,
+  visibility = ['PUBLIC'],
+)
+
+java_test(
+  name = 'httpd_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':httpd',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:easymock',
+    '//lib:junit',
+    '//lib:gson',
+    '//lib:gwtorm',
+    '//lib:guava',
+    '//lib:servlet-api-3_0',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/jgit:junit',
+  ],
+  source_under_test = [':httpd'],
+  # TODO(sop) Remove after Buck supports Eclipse
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
deleted file mode 100644
index e498cac..0000000
--- a/gerrit-httpd/pom.xml
+++ /dev/null
@@ -1,95 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-httpd</artifactId>
-  <name>Gerrit Code Review - HTTPd</name>
-
-  <description>
-    Servlet context for components run inside of an HTTP environment.
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>servlet-api</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.code.findbugs</groupId>
-      <artifactId>jsr305</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit.http.server</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit.junit</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>eu.medsea.mimeutil</groupId>
-      <artifactId>mime-util</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>gwtjsonrpc</groupId>
-      <artifactId>gwtjsonrpc</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-server</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-prettify</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
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 96792f0..9968e2a 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
@@ -185,6 +185,7 @@
   public void setUserAccountId(Account.Id id) {
     key = new Key("id:" + id);
     val = new Val(id, 0, false, null, 0, null, null);
+    user = identified.runAs(id, user);
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java
index 61c0cd1..992c70a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.httpd;
 
-import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
index 407784e..efbef76 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -2,6 +2,7 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.base.CharMatcher;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
@@ -49,7 +50,7 @@
   @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
-    String query = req.getPathInfo();
+    String query = CharMatcher.is('/').trimTrailingFrom(req.getPathInfo());
     HashSet<Change.Id> ids = new HashSet<Change.Id>();
     try {
       ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
@@ -69,7 +70,7 @@
         }
       }
     } catch (QueryParseException e) {
-      log.warn("Received invalid query by URL: /r/" + query, e);
+      log.info("Received invalid query by URL: /r/" + query);
     } catch (OrmException e) {
       log.warn("Cannot process query by URL: /r/" + query, e);
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index 7241624..2980159 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -17,10 +17,12 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.contact.ContactStore;
@@ -35,6 +37,7 @@
 import java.net.MalformedURLException;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletContext;
 
@@ -92,14 +95,19 @@
         config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
         break;
 
-      case CLIENT_SSL_CERT_LDAP:
-      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
       case HTTP:
       case HTTP_LDAP:
+        config.setLoginUrl(cfg.getString("auth", null, "loginurl"));
+        config.setLoginText(cfg.getString("auth", null, "logintext"));
+        break;
+
+      case CLIENT_SSL_CERT_LDAP:
+      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
       case OPENID:
       case OPENID_SSO:
         break;
     }
+    config.setSwitchAccountUrl(cfg.getString("auth", null, "switchAccountUrl"));
     config.setUseContributorAgreements(cfg.getBoolean("auth",
         "contributoragreements", false));
     config.setGitDaemonUrl(cfg.getString("gerrit", null, "canonicalgiturl"));
@@ -115,6 +123,11 @@
         "test", false));
     config.setAnonymousCowardName(anonymousCowardName);
     config.setSuggestFrom(cfg.getInt("suggest", "from", 0));
+    config.setChangeUpdateDelay((int) ConfigUtil.getTimeUnit(
+        cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS));
+    config.setChangeScreen(cfg.getEnum(
+        "gerrit", null, "changeScreen",
+        AccountGeneralPreferences.ChangeScreen.OLD_UI));
 
     config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
     if (config.getReportBugUrl() == null) {
@@ -123,13 +136,16 @@
       config.setReportBugUrl(null);
     }
 
+    config.setGitBasicAuth(authConfig.isGitBasichAuth());
+
     final Set<Account.FieldName> fields = new HashSet<Account.FieldName>();
     for (final Account.FieldName n : Account.FieldName.values()) {
       if (realm.allowsEdit(n)) {
         fields.add(n);
       }
     }
-    if (emailSender != null && emailSender.isEnabled()) {
+    if (emailSender != null && emailSender.isEnabled()
+        && realm.allowsEdit(Account.FieldName.REGISTER_NEW_EMAIL)) {
       fields.add(Account.FieldName.REGISTER_NEW_EMAIL);
     }
     config.setEditableAccountFields(fields);
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 dad9b80..4ecd020 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
@@ -256,7 +256,7 @@
         throws ServiceNotAuthorizedException {
       final ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
 
-      if (!(pc.getCurrentUser() instanceof IdentifiedUser)) {
+      if (!(pc.getCurrentUser().isIdentifiedUser())) {
         // Anonymous users are not permitted to push.
         throw new ServiceNotAuthorizedException();
       }
@@ -309,14 +309,11 @@
       }
 
       if (!rp.isCheckReferencedObjectsAreReachable()) {
-        if (isGet) {
-          rc.advertiseHistory();
-        }
         chain.doFilter(request, response);
         return;
       }
 
-      if (!(pc.getCurrentUser() instanceof IdentifiedUser)) {
+      if (!(pc.getCurrentUser().isIdentifiedUser())) {
         chain.doFilter(request, response);
         return;
       }
@@ -326,7 +323,6 @@
           projectName);
 
       if (isGet) {
-        rc.advertiseHistory();
         cache.invalidate(cacheKey);
       } else {
         Set<ObjectId> ids = cache.getIfPresent(cacheKey);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
index 22d7568..dfb1cc2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
@@ -56,6 +56,7 @@
     type.setRevision(cfg.getString("gitweb", null, "revision"));
     type.setFileHistory(cfg.getString("gitweb", null, "filehistory"));
     type.setLinkDrafts(cfg.getBoolean("gitweb", null, "linkdrafts", true));
+    type.setUrlEncode(cfg.getBoolean("gitweb", null, "urlencode", true));
     String pathSeparator = cfg.getString("gitweb", null, "pathSeparator");
     if (pathSeparator != null) {
       if (pathSeparator.length() == 1) {
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 879dfa3..ddbba3b 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
@@ -14,20 +14,21 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.audit.AuditEvent;
 import com.google.gerrit.audit.AuditService;
-import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import java.io.IOException;
 
-import javax.annotation.Nullable;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -80,7 +81,7 @@
     final String sid = webSession.get().getSessionId();
     final CurrentUser currentUser = webSession.get().getCurrentUser();
     final String what = "sign out";
-    final long when = System.currentTimeMillis();
+    final long when = TimeUtil.nowMs();
 
     try {
       doLogout(req, rsp);
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 3d9f4c8..c55f9d3 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
@@ -53,8 +53,8 @@
  * The current HTTP request is authenticated by looking up the username and
  * password from the Base64 encoded Authorization header and validating them
  * against any username/password configured authentication system in Gerrit.
- * This filter is intended only to protect the {@link ProjectServlet} and its
- * handled URLs, which provide remote repository access over HTTP.
+ * This filter is intended only to protect the {@link GitOverHttpServlet} and
+ * its handled URLs, which provide remote repository access over HTTP.
  *
  * @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
  */
@@ -189,6 +189,7 @@
     }
 
     @Override
+    @Deprecated
     public void setStatus(int sc, String sm) {
       status(sc);
       super.setStatus(sc, sm);
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 c38425d..523309e 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
@@ -20,6 +20,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
@@ -42,7 +43,6 @@
 import java.util.Locale;
 import java.util.Map;
 
-import javax.annotation.Nullable;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -332,6 +332,7 @@
     }
 
     @Override
+    @Deprecated
     public void setStatus(int sc, String sm) {
       status(sc);
       super.setStatus(sc, sm);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
index 499c2a5..2448d3f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -52,7 +51,7 @@
   public void doFilter(ServletRequest request,
       ServletResponse response, FilterChain chain)
       throws IOException, ServletException {
-    if (user.get() instanceof IdentifiedUser) {
+    if (user.get().isIdentifiedUser()) {
       chain.doFilter(request, response);
     } else {
       HttpServletResponse res = (HttpServletResponse) response;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java
index 076a23a..92809c0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -22,7 +23,6 @@
 
 import java.io.IOException;
 
-import javax.annotation.Nullable;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
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
new file mode 100644
index 0000000..e0bef35
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Allows running a request as another user account. */
+@Singleton
+class RunAsFilter implements Filter {
+  private static final Logger log = LoggerFactory.getLogger(RunAsFilter.class);
+  private static final String RUN_AS = "X-Gerrit-RunAs";
+
+  static class Module extends ServletModule {
+    @Override
+    protected void configureServlets() {
+      filter("/*").through(RunAsFilter.class);
+    }
+  }
+
+  private final boolean enabled;
+  private final Provider<WebSession> session;
+  private final AccountResolver accountResolver;
+
+  @Inject
+  RunAsFilter(AuthConfig config,
+      Provider<WebSession> session,
+      AccountResolver accountResolver) {
+    this.enabled = config.isRunAsEnabled();
+    this.session = session;
+    this.accountResolver = accountResolver;
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest req = (HttpServletRequest) request;
+    HttpServletResponse res = (HttpServletResponse) response;
+
+    String runas = req.getHeader(RUN_AS);
+    if (runas != null) {
+      if (!enabled) {
+        RestApiServlet.replyError(req, res,
+            SC_FORBIDDEN,
+            RUN_AS + " disabled by auth.enableRunAs = false");
+        return;
+      }
+
+      CurrentUser self = session.get().getCurrentUser();
+      if (!self.getCapabilities().canRunAs()) {
+        RestApiServlet.replyError(req, res,
+            SC_FORBIDDEN,
+            "not permitted to use " + RUN_AS);
+        return;
+      }
+
+      Account target;
+      try {
+        target = accountResolver.find(runas);
+      } catch (OrmException e) {
+        log.warn("cannot resolve account for " + RUN_AS, e);
+        RestApiServlet.replyError(req, res,
+            SC_INTERNAL_SERVER_ERROR,
+            "cannot resolve " + RUN_AS);
+        return;
+      }
+      if (target == null) {
+        RestApiServlet.replyError(req, res,
+            SC_FORBIDDEN,
+            "no account matches " + RUN_AS);
+        return;
+      }
+      session.get().setUserAccountId(target.getId());
+    }
+
+    chain.doFilter(req, res);
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) {
+  }
+
+  @Override
+  public void destroy() {
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index bd25faf..bf39bfb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -21,12 +21,15 @@
 import com.google.gerrit.httpd.raw.CatServlet;
 import com.google.gerrit.httpd.raw.HostPageServlet;
 import com.google.gerrit.httpd.raw.LegacyGerritServlet;
+import com.google.gerrit.httpd.raw.RobotsServlet;
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 import com.google.gerrit.httpd.raw.StaticServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
+import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
 import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
 import com.google.gerrit.httpd.rpc.change.ChangesRestApiServlet;
 import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
+import com.google.gerrit.httpd.rpc.config.ConfigRestApiServlet;
 import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
 import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
 import com.google.gerrit.reviewdb.client.Change;
@@ -100,14 +103,18 @@
 
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
     serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
+    serveRegex("^/(?:a/)?access/(.*)$").with(AccessRestApiServlet.class);
     serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
     serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
+    serveRegex("^/(?:a/)?config/(.*)$").with(ConfigRestApiServlet.class);
     serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
     serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
 
     if (cfg.deprecatedQuery) {
       serve("/query").with(DeprecatedChangeQueryServlet.class);
     }
+
+    serve("/robots.txt").with(RobotsServlet.class);
   }
 
   private Key<HttpServlet> notFound() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index f7ba6df..5593baf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -27,10 +28,7 @@
 import com.google.gerrit.httpd.gitweb.GitWebModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.RemotePeer;
-import com.google.gerrit.server.account.ClearPassword;
-import com.google.gerrit.server.account.GeneratePassword;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.FactoryModule;
@@ -48,8 +46,6 @@
 
 import java.net.SocketAddress;
 
-import javax.annotation.Nullable;
-
 public class WebModule extends FactoryModule {
   private final AuthConfig authConfig;
   private final UrlModule.UrlConfig urlConfig;
@@ -85,11 +81,12 @@
     if (wantSSL) {
       install(new RequireSslFilter.Module());
     }
+    install(new RunAsFilter.Module());
 
     switch (authConfig.getAuthType()) {
       case HTTP:
       case HTTP_LDAP:
-        install(new HttpAuthModule());
+        install(new HttpAuthModule(authConfig));
         break;
 
       case CLIENT_SSL_CERT_LDAP:
@@ -130,10 +127,7 @@
     bind(GerritConfig.class).toProvider(GerritConfigProvider.class);
     DynamicSet.setOf(binder(), WebUiPlugin.class);
 
-    factory(ClearPassword.Factory.class);
     install(new AsyncReceiveCommits.Module());
-    install(new CmdLineParserModule());
-    factory(GeneratePassword.Factory.class);
 
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
         HttpRemotePeerProvider.class).in(RequestScoped.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index a5338f8..03eca9f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -22,6 +22,7 @@
 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;
+import static com.google.gerrit.server.util.TimeUtil.nowMs;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
@@ -53,10 +54,6 @@
   private static final Logger log = LoggerFactory.getLogger(WebSessionManager.class);
   static final String CACHE_NAME = "web_sessions";
 
-  static long now() {
-    return System.currentTimeMillis();
-  }
-
   private final long sessionMaxAgeMillis;
   private final SecureRandom prng;
   private final Cache<String, Val> self;
@@ -117,7 +114,7 @@
     final long halfAgeRefresh = sessionMaxAgeMillis >>> 1;
     final long minRefresh = MILLISECONDS.convert(1, HOURS);
     final long refresh = Math.min(halfAgeRefresh, minRefresh);
-    final long now = now();
+    final long now = nowMs();
     final long refreshCookieAt = now + refresh;
     final long expiresAt = now + sessionMaxAgeMillis;
     if (sid == null) {
@@ -150,7 +147,7 @@
 
   Val get(final Key key) {
     Val val = self.getIfPresent(key.token);
-    if (val != null && val.expiresAt <= now()) {
+    if (val != null && val.expiresAt <= nowMs()) {
       self.invalidate(key.token);
       return null;
     }
@@ -223,7 +220,7 @@
     }
 
     boolean needsCookieRefresh() {
-      return refreshCookieAt <= now();
+      return refreshCookieAt <= nowMs();
     }
 
     boolean isPersistentCookie() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index adca95e..75a7e07 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -61,6 +61,8 @@
   private final byte[] signInRaw;
   private final byte[] signInGzip;
   private final String loginHeader;
+  private final String displaynameHeader;
+  private final String emailHeader;
 
   @Inject
   HttpAuthFilter(final Provider<WebSession> webSession,
@@ -78,6 +80,8 @@
     loginHeader = firstNonNull(
         emptyToNull(authConfig.getLoginHttpHeader()),
         AUTHORIZATION);
+    displaynameHeader = emptyToNull(authConfig.getHttpDisplaynameHeader());
+    emailHeader = emptyToNull(authConfig.getHttpEmailHeader());
   }
 
   @Override
@@ -174,6 +178,22 @@
     }
   }
 
+  String getRemoteDisplayname(HttpServletRequest req) {
+    if (displaynameHeader != null) {
+      return emptyToNull(req.getHeader(displaynameHeader));
+    } else {
+      return null;
+    }
+  }
+
+  String getRemoteEmail(HttpServletRequest req) {
+    if (emailHeader != null) {
+      return emptyToNull(req.getHeader(emailHeader));
+    } else {
+      return null;
+    }
+  }
+
   String getLoginHeader() {
     return loginHeader;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
index daaa7e2..638d527 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
@@ -14,13 +14,22 @@
 
 package com.google.gerrit.httpd.auth.container;
 
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.servlet.ServletModule;
 
 /** Servlets and support related to HTTP authentication. */
 public class HttpAuthModule extends ServletModule {
+  private final AuthConfig authConfig;
+
+  public HttpAuthModule(final AuthConfig authConfig) {
+    this.authConfig = authConfig;
+  }
+
   @Override
   protected void configureServlets() {
-    filter("/").through(HttpAuthFilter.class);
+    if (authConfig.getLoginUrl() == null) {
+      filter("/").through(HttpAuthFilter.class);
+    }
     serve("/login", "/login/*").with(HttpLoginServlet.class);
   }
 }
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 d2a25a8..e62fde5 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -59,16 +60,19 @@
   private final CanonicalWebUrl urlProvider;
   private final AccountManager accountManager;
   private final HttpAuthFilter authFilter;
+  private final AuthConfig authConfig;
 
   @Inject
   HttpLoginServlet(final Provider<WebSession> webSession,
       final CanonicalWebUrl urlProvider,
       final AccountManager accountManager,
-      final HttpAuthFilter authFilter) {
+      final HttpAuthFilter authFilter,
+      final AuthConfig authConfig) {
     this.webSession = webSession;
     this.urlProvider = urlProvider;
     this.accountManager = accountManager;
     this.authFilter = authFilter;
+    this.authConfig = authConfig;
   }
 
   @Override
@@ -110,6 +114,8 @@
     }
 
     final AuthRequest areq = AuthRequest.forUser(user);
+    areq.setDisplayName(authFilter.getRemoteDisplayname(req));
+    areq.setEmailAddress(authFilter.getRemoteEmail(req));
     final AuthResult arsp;
     try {
       arsp = accountManager.authenticate(areq);
@@ -120,12 +126,16 @@
     }
 
     final StringBuilder rdr = new StringBuilder();
-    rdr.append(urlProvider.get(req));
-    rdr.append('#');
-    if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + "/")) {
-      rdr.append(PageLinks.REGISTER);
+    if (arsp.isNew() && authConfig.getRegisterPageUrl() != null) {
+      rdr.append(authConfig.getRegisterPageUrl());
+    } else {
+      rdr.append(urlProvider.get(req));
+      rdr.append('#');
+      if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + "/")) {
+        rdr.append(PageLinks.REGISTER);
+      }
+      rdr.append(token);
     }
-    rdr.append(token);
 
     webSession.get().login(arsp, true /* persistent cookie */);
     rsp.sendRedirect(rdr.toString());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
index d9585b9..3c3174d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.auth.container;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gwtexpui.server.CacheHeaders;
@@ -23,7 +24,6 @@
 
 import java.io.IOException;
 
-import javax.annotation.Nullable;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
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 037c7bb..24dd5bf 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
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.HtmlDomUtil;
@@ -39,7 +40,6 @@
 
 import java.io.IOException;
 
-import javax.annotation.Nullable;
 import javax.servlet.ServletException;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServlet;
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 3be0cd3..0769865 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
@@ -518,7 +518,7 @@
     }
 
     String remoteUser = null;
-    if (project.getCurrentUser() instanceof IdentifiedUser) {
+    if (project.getCurrentUser().isIdentifiedUser()) {
       final IdentifiedUser u = (IdentifiedUser) project.getCurrentUser();
       final String user = u.getUserName();
       env.set("GERRIT_USER_NAME", user);
@@ -619,7 +619,6 @@
     }, "GitWeb-ErrorLogger").start();
   }
 
-  @SuppressWarnings("unchecked")
   private static Enumeration<String> enumerateHeaderNames(HttpServletRequest req) {
     return req.getHeaderNames();
   }
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 d3693a5..b2972d9 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
@@ -44,8 +44,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
@@ -63,6 +65,7 @@
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
@@ -176,7 +179,7 @@
       }
 
       try {
-        WrappedContext ctx = new WrappedContext(plugin, base + name);
+        ServletContext ctx = PluginServletContext.create(plugin, base + name);
         filter.init(new WrappedFilterConfig(ctx));
       } catch (ServletException e) {
         log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
@@ -309,11 +312,39 @@
     }
   }
 
+  private void appendEntriesSection(JarFile jar, List<JarEntry> entries,
+      String sectionTitle, StringBuilder md, String prefix,
+      int nameOffset) throws IOException {
+    if (!entries.isEmpty()) {
+      md.append("## " + sectionTitle +  " ##\n");
+      for(JarEntry entry : entries) {
+        String rsrc = entry.getName().substring(prefix.length());
+        String entryTitle;
+        if (rsrc.endsWith(".html")) {
+          entryTitle = rsrc.substring(nameOffset, rsrc.length() - 5).replace('-', ' ');
+        } else if (rsrc.endsWith(".md")) {
+          entryTitle = extractTitleFromMarkdown(jar, entry);
+          if (Strings.isNullOrEmpty(entryTitle)) {
+            entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
+          }
+          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+        } else {
+          entryTitle = rsrc.substring(nameOffset).replace('-', ' ');
+        }
+        md.append(String.format("* [%s](%s)\n", entryTitle, rsrc));
+      }
+      md.append("\n");
+    }
+  }
+
   private void sendAutoIndex(JarFile jar,
       String prefix, String pluginName,
       ResourceKey cacheKey, HttpServletResponse res) throws IOException {
     List<JarEntry> cmds = Lists.newArrayList();
+    List<JarEntry> servlets = Lists.newArrayList();
+    List<JarEntry> restApis = Lists.newArrayList();
     List<JarEntry> docs = Lists.newArrayList();
+    JarEntry about = null;
     Enumeration<JarEntry> entries = jar.entries();
     while (entries.hasMoreElements()) {
       JarEntry entry = entries.nextElement();
@@ -323,8 +354,17 @@
           && (name.endsWith(".md")
               || name.endsWith(".html"))
           && 0 < size && size <= SMALL_RESOURCE) {
-        if (name.substring(prefix.length()).startsWith("cmd-")) {
+        name = name.substring(prefix.length());
+        if (name.startsWith("cmd-")) {
           cmds.add(entry);
+        } else if (name.startsWith("servlet-")) {
+          servlets.add(entry);
+        } else if (name.startsWith("rest-api-")) {
+          restApis.add(entry);
+        } else if (name.startsWith("about.")) {
+          if (about == null) {
+            about = entry;
+          }
         } else {
           docs.add(entry);
         }
@@ -348,47 +388,32 @@
     md.append("\n");
     appendPluginInfoTable(md, jar.getManifest().getMainAttributes());
 
-    if (!docs.isEmpty()) {
-      md.append("## Documentation ##\n");
-      for(JarEntry entry : docs) {
-        String rsrc = entry.getName().substring(prefix.length());
-        String title;
-        if (rsrc.endsWith(".html")) {
-          title = rsrc.substring(0, rsrc.length() - 5).replace('-', ' ');
-        } else if (rsrc.endsWith(".md")) {
-          title = extractTitleFromMarkdown(jar, entry);
-          if (Strings.isNullOrEmpty(title)) {
-            title = rsrc.substring(0, rsrc.length() - 3).replace('-', ' ');
-          }
-          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+    if (about != null) {
+      InputStreamReader isr = new InputStreamReader(jar.getInputStream(about));
+      BufferedReader reader = new BufferedReader(isr);
+      StringBuilder aboutContent = new StringBuilder();
+      String line;
+      while ((line = reader.readLine()) != null) {
+        line = line.trim();
+        if (line.isEmpty()) {
+          aboutContent.append("\n");
         } else {
-          title = rsrc.replace('-', ' ');
+          aboutContent.append(line).append("\n");
         }
-        md.append(String.format("* [%s](%s)\n", title, rsrc));
       }
-      md.append("\n");
+      reader.close();
+
+      // Only append the About section if there was anything in it
+      if (aboutContent.toString().trim().length() > 0) {
+        md.append("## About ##\n");
+        md.append("\n").append(aboutContent);
+      }
     }
 
-    if (!cmds.isEmpty()) {
-      md.append("## Commands ##\n");
-      for(JarEntry entry : cmds) {
-        String rsrc = entry.getName().substring(prefix.length());
-        String title;
-        if (rsrc.endsWith(".html")) {
-          title = rsrc.substring(4, rsrc.length() - 5).replace('-', ' ');
-        } else if (rsrc.endsWith(".md")) {
-          title = extractTitleFromMarkdown(jar, entry);
-          if (Strings.isNullOrEmpty(title)) {
-            title = rsrc.substring(4, rsrc.length() - 3).replace('-', ' ');
-          }
-          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
-        } else {
-          title = rsrc.substring(4).replace('-', ' ');
-        }
-        md.append(String.format("* [%s](%s)\n", title, rsrc));
-      }
-      md.append("\n");
-    }
+    appendEntriesSection(jar, docs, "Documentation", md, prefix, 0);
+    appendEntriesSection(jar, servlets, "Servlets", md, prefix, "servlet-".length());
+    appendEntriesSection(jar, restApis, "REST APIs", md, prefix, "rest-api-".length());
+    appendEntriesSection(jar, cmds, "Commands", md, prefix, "cmd-".length());
 
     sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res);
   }
@@ -633,8 +658,16 @@
 
     @Override
     public String getServletPath() {
-      return ((HttpServletRequest) getRequest()).getRequestURI().substring(
-          contextPath.length());
+      return getRequestURI().substring(contextPath.length());
+    }
+
+    @Override
+    public String getRequestURI() {
+      String uri = super.getRequestURI();
+      if (uri.startsWith("/a/")) {
+        uri = uri.substring(2);
+      }
+      return uri;
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
new file mode 100644
index 0000000..c3395b5
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
@@ -0,0 +1,241 @@
+// 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.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.plugins.Plugin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+
+class PluginServletContext {
+  private static final Logger log = LoggerFactory.getLogger("plugin");
+
+  static ServletContext create(Plugin plugin, String contextPath) {
+    return (ServletContext) Proxy.newProxyInstance(
+        PluginServletContext.class.getClassLoader(),
+        new Class[] {ServletContext.class, API.class},
+        new Handler(plugin, contextPath));
+  }
+
+  private PluginServletContext() {
+  }
+
+  private static class Handler implements InvocationHandler, API {
+    private final Plugin plugin;
+    private final String contextPath;
+    private final ConcurrentMap<String, Object> attributes;
+
+    Handler(Plugin plugin, String contextPath) {
+      this.plugin = plugin;
+      this.contextPath = contextPath;
+      this.attributes = Maps.newConcurrentMap();
+    }
+
+    @Override
+    public Object invoke(Object proxy, Method method, Object[] args)
+        throws Throwable {
+      Method handler;
+      try {
+        handler = API.class.getDeclaredMethod(
+            method.getName(),
+            method.getParameterTypes());
+      } catch (NoSuchMethodException e) {
+        throw new NoSuchMethodError(String.format(
+            "%s does not implement%s",
+            PluginServletContext.class,
+            method.toGenericString()));
+      }
+      return handler.invoke(this, args);
+    }
+
+    @Override
+    public String getContextPath() {
+      return contextPath;
+    }
+
+    @Override
+    public String getInitParameter(String name) {
+      return null;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public Enumeration getInitParameterNames() {
+      return Collections.enumeration(Collections.emptyList());
+    }
+
+    @Override
+    public ServletContext getContext(String name) {
+      return null;
+    }
+
+    @Override
+    public RequestDispatcher getNamedDispatcher(String name) {
+      return null;
+    }
+
+    @Override
+    public RequestDispatcher getRequestDispatcher(String name) {
+      return null;
+    }
+
+    @Override
+    public URL getResource(String name) {
+      return null;
+    }
+
+    @Override
+    public InputStream getResourceAsStream(String name) {
+      return null;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public Set getResourcePaths(String name) {
+      return null;
+    }
+
+    @Override
+    public Servlet getServlet(String name) {
+      return null;
+    }
+
+    @Override
+    public String getRealPath(String name) {
+      return null;
+    }
+
+    @Override
+    public String getServletContextName() {
+      return plugin.getName();
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public Enumeration getServletNames() {
+      return Collections.enumeration(Collections.emptyList());
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public Enumeration getServlets() {
+      return Collections.enumeration(Collections.emptyList());
+    }
+
+    @Override
+    public void log(Exception reason, String msg) {
+      log(msg, reason);
+    }
+
+    @Override
+    public void log(String msg) {
+      log(msg, null);
+    }
+
+    @Override
+    public void log(String msg, Throwable reason) {
+      log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
+    }
+
+    @Override
+    public Object getAttribute(String name) {
+      return attributes.get(name);
+    }
+
+    @Override
+    public Enumeration<String> getAttributeNames() {
+      return Collections.enumeration(attributes.keySet());
+    }
+
+    @Override
+    public void setAttribute(String name, Object value) {
+      attributes.put(name, value);
+    }
+
+    @Override
+    public void removeAttribute(String name) {
+      attributes.remove(name);
+    }
+
+    @Override
+    public String getMimeType(String file) {
+      return null;
+    }
+
+    @Override
+    public int getMajorVersion() {
+      return 2;
+    }
+
+    @Override
+    public int getMinorVersion() {
+      return 5;
+    }
+
+    @Override
+    public String getServerInfo() {
+      String v = Version.getVersion();
+      return "Gerrit Code Review/" + (v != null ? v : "dev");
+    }
+  }
+
+  static interface API {
+    String getContextPath();
+    String getInitParameter(String name);
+    @SuppressWarnings("rawtypes")
+    Enumeration getInitParameterNames();
+    ServletContext getContext(String name);
+    RequestDispatcher getNamedDispatcher(String name);
+    RequestDispatcher getRequestDispatcher(String name);
+    URL getResource(String name);
+    InputStream getResourceAsStream(String name);
+    @SuppressWarnings("rawtypes")
+    Set getResourcePaths(String name);
+    Servlet getServlet(String name);
+    String getRealPath(String name);
+    String getServletContextName();
+    @SuppressWarnings("rawtypes")
+    Enumeration getServletNames();
+    @SuppressWarnings("rawtypes")
+    Enumeration getServlets();
+    void log(Exception reason, String msg);
+    void log(String msg);
+    void log(String msg, Throwable reason);
+    Object getAttribute(String name);
+    Enumeration<String> getAttributeNames();
+    void setAttribute(String name, Object value);
+    void removeAttribute(String name);
+    String getMimeType(String file);
+    int getMajorVersion();
+    int getMinorVersion();
+    String getServerInfo();
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
index e408f72..0db7a88 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.gerrit.common.Nullable;
+
 import java.io.IOException;
 
-import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
deleted file mode 100644
index daeb6ff..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
+++ /dev/null
@@ -1,178 +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.httpd.plugins;
-
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.Version;
-import com.google.gerrit.server.plugins.Plugin;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.Set;
-import java.util.concurrent.ConcurrentMap;
-
-import javax.servlet.RequestDispatcher;
-import javax.servlet.Servlet;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-
-class WrappedContext implements ServletContext {
-  private static final Logger log = LoggerFactory.getLogger("plugin");
-  private final Plugin plugin;
-  private final String contextPath;
-  private final ConcurrentMap<String, Object> attributes;
-
-  WrappedContext(Plugin plugin, String contextPath) {
-    this.plugin = plugin;
-    this.contextPath = contextPath;
-    this.attributes = Maps.newConcurrentMap();
-  }
-
-  @Override
-  public String getContextPath() {
-    return contextPath;
-  }
-
-  @Override
-  public String getInitParameter(String name) {
-    return null;
-  }
-
-  @SuppressWarnings("rawtypes")
-  @Override
-  public Enumeration getInitParameterNames() {
-    return Collections.enumeration(Collections.emptyList());
-  }
-
-  @Override
-  public ServletContext getContext(String name) {
-    return null;
-  }
-
-  @Override
-  public RequestDispatcher getNamedDispatcher(String name) {
-    return null;
-  }
-
-  @Override
-  public RequestDispatcher getRequestDispatcher(String name) {
-    return null;
-  }
-
-  @Override
-  public URL getResource(String name) throws MalformedURLException {
-    return null;
-  }
-
-  @Override
-  public InputStream getResourceAsStream(String name) {
-    return null;
-  }
-
-  @SuppressWarnings("rawtypes")
-  @Override
-  public Set getResourcePaths(String name) {
-    return null;
-  }
-
-  @Override
-  public Servlet getServlet(String name) throws ServletException {
-    return null;
-  }
-
-  @Override
-  public String getRealPath(String name) {
-    return null;
-  }
-
-  @Override
-  public String getServletContextName() {
-    return plugin.getName();
-  }
-
-  @SuppressWarnings("rawtypes")
-  @Override
-  public Enumeration getServletNames() {
-    return Collections.enumeration(Collections.emptyList());
-  }
-
-  @SuppressWarnings("rawtypes")
-  @Override
-  public Enumeration getServlets() {
-    return Collections.enumeration(Collections.emptyList());
-  }
-
-  @Override
-  public void log(Exception reason, String msg) {
-    log(msg, reason);
-  }
-
-  @Override
-  public void log(String msg) {
-    log(msg, null);
-  }
-
-  @Override
-  public void log(String msg, Throwable reason) {
-    log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
-  }
-
-  @Override
-  public Object getAttribute(String name) {
-    return attributes.get(name);
-  }
-
-  @Override
-  public Enumeration<String> getAttributeNames() {
-    return Collections.enumeration(attributes.keySet());
-  }
-
-  @Override
-  public void setAttribute(String name, Object value) {
-    attributes.put(name, value);
-  }
-
-  @Override
-  public void removeAttribute(String name) {
-    attributes.remove(name);
-  }
-
-  @Override
-  public String getMimeType(String file) {
-    return null;
-  }
-
-  @Override
-  public int getMajorVersion() {
-    return 2;
-  }
-
-  @Override
-  public int getMinorVersion() {
-    return 5;
-  }
-
-  @Override
-  public String getServerInfo() {
-    String v = Version.getVersion();
-    return "Gerrit Code Review/" + (v != null ? v : "dev");
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
index c9107dc..04e49c9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
@@ -23,9 +23,9 @@
 import javax.servlet.ServletContext;
 
 class WrappedFilterConfig implements FilterConfig {
-  private final WrappedContext context;
+  private final ServletContext context;
 
-  WrappedFilterConfig(WrappedContext context) {
+  WrappedFilterConfig(ServletContext context) {
     this.context = context;
   }
 
@@ -39,7 +39,7 @@
     return null;
   }
 
-  @SuppressWarnings("rawtypes")
+  @SuppressWarnings({"rawtypes", "unchecked"})
   @Override
   public Enumeration getInitParameterNames() {
     return Collections.enumeration(Collections.emptyList());
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 1ecd929..47a9263 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -333,7 +334,7 @@
     md.update(req.getRemoteAddr().getBytes("UTF-8"));
     md.update(buf, 0, 4);
 
-    NB.encodeInt64(buf, 0, System.currentTimeMillis());
+    NB.encodeInt64(buf, 0, TimeUtil.nowMs());
     md.update(buf, 0, 8);
 
     rng.nextBytes(buf);
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 ea2168a..508c41a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.httpd.raw;
 
+import com.google.common.collect.Lists;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
-import com.google.common.collect.Lists;
 import com.google.common.primitives.Bytes;
+import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -42,6 +43,7 @@
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
+import org.w3c.dom.Node;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -79,6 +81,7 @@
   private final String noCacheName;
   private final PermutationSelector selector;
   private final boolean refreshHeaderFooter;
+  private final StaticServlet staticServlet;
   private volatile Page page;
 
   @Inject
@@ -86,7 +89,8 @@
       final SitePaths sp, final ThemeFactory themeFactory,
       final GerritConfig gc, final ServletContext servletContext,
       final DynamicSet<WebUiPlugin> webUiPlugins,
-      @GerritServerConfig final Config cfg)
+      @GerritServerConfig final Config cfg,
+      final StaticServlet ss)
       throws IOException, ServletException {
     currentUser = cu;
     session = w;
@@ -96,6 +100,7 @@
     signedInTheme = themeFactory.getSignedInTheme();
     site = sp;
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    staticServlet = ss;
     boolean checkUserAgent = cfg.getBoolean("site", "checkUserAgent", true);
 
     final String pageName = "HostPage.html";
@@ -174,7 +179,7 @@
     final Page.Content page = select(req);
     final StringWriter w = new StringWriter();
     final CurrentUser user = currentUser.get();
-    if (user instanceof IdentifiedUser) {
+    if (user.isIdentifiedUser()) {
       w.write(HPD_ID + ".account=");
       json(((IdentifiedUser) user).getAccount(), w);
       w.write(";");
@@ -246,6 +251,26 @@
     return pg.get(selector.select(req));
   }
 
+  private void insertETags(Element e) {
+    if ("img".equalsIgnoreCase(e.getTagName())
+        || "script".equalsIgnoreCase(e.getTagName())) {
+      String src = e.getAttribute("src");
+      if (src != null && src.startsWith("static/")) {
+        String name = src.substring("static/".length());
+        StaticServlet.Resource r = staticServlet.getResource(name);
+        if (r != null) {
+          e.setAttribute("src", src + "?e=" + r.etag);
+        }
+      }
+    }
+
+    for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
+      if (n instanceof Element) {
+        insertETags((Element) n);
+      }
+    }
+  }
+
   private static class FileInfo {
     private final File path;
     private final long time;
@@ -275,6 +300,7 @@
       footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer);
 
       final HostPageData pageData = new HostPageData();
+      pageData.version = Version.getVersion();
       pageData.config = config;
 
       final StringWriter w = new StringWriter();
@@ -376,7 +402,8 @@
         return info;
       }
 
-      final Element content = html.getDocumentElement();
+      Element content = html.getDocumentElement();
+      insertETags(content);
       banner.appendChild(hostDoc.importNode(content, true));
       return info;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
new file mode 100644
index 0000000..d19a0ce
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This class provides a mechanism to use a configurable robots.txt file,
+ * outside of the .war of the application. In order to configure it add the
+ * following to the <code>httpd</code> section of the <code>gerrit.conf</code>
+ * file:
+ *
+ * <pre>
+ * [httpd]
+ *         robotsFile = etc/myrobots.txt
+ * </pre>
+ *
+ * If the specified file name is relative it will resolved as a sub directory of
+ * the site directory, if it is absolute it will be used as is.
+ *
+ * If the specified file doesn't exist or isn't readable the servlet will
+ * default to the <code>robots.txt</code> file bundled with the .war file of the
+ * application.
+ */
+@SuppressWarnings("serial")
+@Singleton
+public class RobotsServlet extends HttpServlet {
+  private static final Logger log =
+      LoggerFactory.getLogger(RobotsServlet.class);
+
+  private final File robotsFile;
+
+  @Inject
+  RobotsServlet(@GerritServerConfig final Config config, final SitePaths sitePaths) {
+    File file = sitePaths.resolve(
+      config.getString("httpd", null, "robotsFile"));
+    if (file != null && (!file.exists() || !file.canRead())) {
+      log.warn("Cannot read httpd.robotsFile, using default");
+      file = null;
+    }
+    robotsFile = file;
+  }
+
+  @Override
+  protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp)
+      throws IOException {
+    rsp.setContentType("text/plain");
+    InputStream in = openRobotsFile();
+    try {
+      OutputStream out = rsp.getOutputStream();
+      try {
+        ByteStreams.copy(in, out);
+      } finally {
+        out.close();
+      }
+    } finally {
+      in.close();
+    }
+  }
+
+  private InputStream openRobotsFile() {
+    if (robotsFile != null) {
+      try {
+        return new FileInputStream(robotsFile);
+      } catch (IOException e) {
+        log.warn("Cannot read " + robotsFile + "; using default", e);
+      }
+    }
+    return getServletContext().getResourceAsStream("/robots.txt");
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
index bc0a174..94f8c1b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
@@ -14,37 +14,60 @@
 
 package com.google.gerrit.httpd.raw;
 
+import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
+import static com.google.common.net.HttpHeaders.ETAG;
+import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
 import com.google.common.collect.Maps;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.zip.GZIPOutputStream;
+import java.util.concurrent.ExecutionException;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+
 /** Sends static content from the site 's <code>static/</code> subdirectory. */
 @SuppressWarnings("serial")
 @Singleton
 public class StaticServlet extends HttpServlet {
+  private static final Logger log = LoggerFactory.getLogger(StaticServlet.class);
+  private static final String JS = "application/x-javascript";
   private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
   static {
     MIME_TYPES.put("html", "text/html");
     MIME_TYPES.put("htm", "text/html");
-    MIME_TYPES.put("js", "application/x-javascript");
+    MIME_TYPES.put("js", JS);
     MIME_TYPES.put("css", "text/css");
     MIME_TYPES.put("rtf", "text/rtf");
     MIME_TYPES.put("txt", "text/plain");
@@ -66,31 +89,13 @@
     return type != null ? type : "application/octet-stream";
   }
 
-  private static byte[] readFile(final File p) throws IOException {
-    final FileInputStream in = new FileInputStream(p);
-    try {
-      final byte[] r = new byte[(int) in.getChannel().size()];
-      IO.readFully(in, r, 0, r.length);
-      return r;
-    } finally {
-      in.close();
-    }
-  }
-
-  private static byte[] compress(final byte[] raw) throws IOException {
-    final ByteArrayOutputStream out = new ByteArrayOutputStream();
-    final GZIPOutputStream gz = new GZIPOutputStream(out);
-    gz.write(raw);
-    gz.finish();
-    gz.flush();
-    return out.toByteArray();
-  }
-
   private final File staticBase;
   private final String staticBasePath;
+  private final boolean refresh;
+  private final LoadingCache<String, Resource> cache;
 
   @Inject
-  StaticServlet(final SitePaths site) {
+  StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
     File f;
     try {
       f = site.static_dir.getCanonicalFile();
@@ -99,70 +104,101 @@
     }
     staticBase = f;
     staticBasePath = staticBase.getPath() + File.separator;
+    refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    cache = CacheBuilder.newBuilder()
+        .maximumWeight(1 << 20)
+        .weigher(new Weigher<String, Resource>() {
+          @Override
+          public int weigh(String name, Resource r) {
+            return 2 * name.length() + r.raw.length;
+          }
+        })
+        .build(new CacheLoader<String, Resource>() {
+          @Override
+          public Resource load(String name) throws Exception {
+            return loadResource(name);
+          }
+        });
   }
 
-  private File local(final HttpServletRequest req) {
-    final String name = req.getPathInfo();
-    if (name.length() < 2 || !name.startsWith("/") || isUnreasonableName(name)) {
-      // Too short to be a valid file name, or doesn't start with
-      // the path info separator like we expected.
-      //
+  @Nullable
+  Resource getResource(String name) {
+    try {
+      return cache.get(name);
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot load static resource %s", name), e);
       return null;
     }
+  }
 
-    final File p = new File(staticBase, name.substring(1));
-
-    // Ensure that the requested file is *actually* within the static dir base.
-    try {
-      if (!p.getCanonicalFile().getPath().startsWith(staticBasePath))
-        return null;
-    } catch (IOException e) {
-        return null;
+  private Resource getResource(HttpServletRequest req) throws ExecutionException {
+    String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
+    if (isUnreasonableName(name)) {
+      return Resource.NOT_FOUND;
     }
 
-    return p.isFile() ? p : null;
+    Resource r = cache.get(name);
+    if (r == Resource.NOT_FOUND) {
+      return Resource.NOT_FOUND;
+    }
+
+    if (refresh && r.isStale()) {
+      cache.invalidate(name);
+      r = cache.get(name);
+    }
+    return r;
   }
 
   private static boolean isUnreasonableName(String name) {
-    if (name.charAt(name.length() -1) == '/') return true; // no suffix
+    if (name.length() < 1) return true;
     if (name.indexOf('\\') >= 0) return true; // no windows/dos stlye paths
     if (name.startsWith("../")) return true; // no "../etc/passwd"
     if (name.contains("/../")) return true; // no "foo/../etc/passwd"
     if (name.contains("/./")) return true; // "foo/./foo" is insane to ask
     if (name.contains("//")) return true; // windows UNC path can be "//..."
-
     return false; // is a reasonable name
   }
 
   @Override
-  protected long getLastModified(final HttpServletRequest req) {
-    final File p = local(req);
-    return p != null ? p.lastModified() : -1;
-  }
-
-  @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
-    final File p = local(req);
-    if (p == null) {
+    Resource r;
+    try {
+      r = getResource(req);
+    } catch (ExecutionException e) {
+      log.warn(String.format(
+          "Cannot load static resource %s",
+          req.getPathInfo()), e);
       CacheHeaders.setNotCacheable(rsp);
-      rsp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+      rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
       return;
     }
 
-    final String type = contentType(p.getName());
-    final byte[] tosend;
-    if (!type.equals("application/x-javascript")
-        && RPCServletUtils.acceptsGzipEncoding(req)) {
-      rsp.setHeader("Content-Encoding", "gzip");
-      tosend = compress(readFile(p));
-    } else {
-      tosend = readFile(p);
+    String e = req.getParameter("e");
+    if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) {
+      CacheHeaders.setNotCacheable(rsp);
+      rsp.setStatus(SC_NOT_FOUND);
+      return;
+    } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+      rsp.setStatus(SC_NOT_MODIFIED);
+      return;
     }
 
-    CacheHeaders.setCacheable(req, rsp, 12, TimeUnit.HOURS);
-    rsp.setDateHeader("Last-Modified", p.lastModified());
-    rsp.setContentType(type);
+    byte[] tosend = r.raw;
+    if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
+      byte[] gz = HtmlDomUtil.compress(tosend);
+      if ((gz.length + 24) < tosend.length) {
+        rsp.setHeader(CONTENT_ENCODING, "gzip");
+        tosend = gz;
+      }
+    }
+    if (e != null && r.etag.equals(e)) {
+      CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
+    } else {
+      CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
+    }
+    rsp.setHeader(ETAG, r.etag);
+    rsp.setContentType(r.contentType);
     rsp.setContentLength(tosend.length);
     final OutputStream out = rsp.getOutputStream();
     try {
@@ -171,4 +207,54 @@
       out.close();
     }
   }
+
+  private Resource loadResource(String name) throws IOException {
+    File p = new File(staticBase, name);
+    try {
+      p = p.getCanonicalFile();
+    } catch (IOException e) {
+      return Resource.NOT_FOUND;
+    }
+    if (!p.getPath().startsWith(staticBasePath)) {
+      return Resource.NOT_FOUND;
+    }
+
+    long ts = p.lastModified();
+    FileInputStream in;
+    try {
+      in = new FileInputStream(p);
+    } catch (FileNotFoundException e) {
+      return Resource.NOT_FOUND;
+    }
+
+    byte[] raw;
+    try {
+      raw = ByteStreams.toByteArray(in);
+    } finally {
+      in.close();
+    }
+    return new Resource(p, ts, contentType(name), raw);
+  }
+
+  static class Resource {
+    static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
+
+    final File src;
+    final long lastModified;
+    final String contentType;
+    final String etag;
+    final byte[] raw;
+
+    Resource(File src, long lastModified, String contentType, byte[] raw) {
+      this.src = src;
+      this.lastModified = lastModified;
+      this.contentType = contentType;
+      this.etag = Hashing.md5().hashBytes(raw).toString();
+      this.raw = raw;
+    }
+
+    boolean isStale() {
+      return lastModified != src.lastModified();
+    }
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index dabffc0..9183d5c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -33,6 +33,7 @@
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.CmdLineException;
@@ -67,7 +68,7 @@
       clp.parseOptionMap(in);
     } catch (CmdLineException e) {
       if (!clp.wasHelpRequestedByOption()) {
-        replyError(res, SC_BAD_REQUEST, e.getMessage());
+        replyError(req, res, SC_BAD_REQUEST, e.getMessage());
         return false;
       }
     }
@@ -79,6 +80,7 @@
       msg.write('\n');
       clp.printUsage(msg, null);
       msg.write('\n');
+      CacheHeaders.setNotCacheable(res);
       replyBinaryResult(req, res,
           BinaryResult.create(msg.toString()).setContentType("text/plain"));
       return false;
@@ -159,7 +161,6 @@
    */
   static JsonObject formToJson(HttpServletRequest req)
       throws BadRequestException {
-    @SuppressWarnings("unchecked")
     Map<String, String[]> map = req.getParameterMap();
     return formToJson(map, query(req));
   }
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 1040da3..517b017 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
@@ -16,6 +16,9 @@
 
 import static com.google.common.base.Charsets.UTF_8;
 import static com.google.common.base.Preconditions.checkNotNull;
+
+import static java.math.RoundingMode.CEILING;
+
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
@@ -23,9 +26,11 @@
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 
+import com.google.common.base.Charsets;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
@@ -38,21 +43,24 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
+import com.google.common.io.BaseEncoding;
+import com.google.common.math.IntMath;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.audit.HttpAuditEvent;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.PreconditionFailedException;
-import com.google.gerrit.extensions.restapi.PutInput;
+import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -61,7 +69,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.extensions.restapi.StreamingResponse;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.httpd.WebSession;
@@ -70,7 +77,8 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.CapabilityUtils;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
 import com.google.gson.FieldNamingPolicy;
@@ -105,6 +113,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
+import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -112,7 +121,6 @@
 import java.util.regex.Pattern;
 import java.util.zip.GZIPOutputStream;
 
-import javax.annotation.Nullable;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -180,8 +188,7 @@
   @Override
   protected final void service(HttpServletRequest req, HttpServletResponse res)
       throws ServletException, IOException {
-    long auditStartTs = System.currentTimeMillis();
-    CacheHeaders.setNotCacheable(res);
+    long auditStartTs = TimeUtil.nowMs();
     res.setHeader("Content-Disposition", "attachment");
     res.setHeader("X-Content-Type-Options", "nosniff");
     int status = SC_OK;
@@ -194,17 +201,18 @@
 
       List<IdString> path = splitPath(req);
       RestCollection<RestResource, RestResource> rc = members.get();
-      checkAccessAnnotations(rc.getClass());
+      CapabilityUtils.checkRequiresCapability(globals.currentUser,
+          null, rc.getClass());
 
       RestResource rsrc = TopLevelResource.INSTANCE;
-      RestView<RestResource> view = null;
+      ViewData viewData = new ViewData(null, null);
       if (path.isEmpty()) {
         if ("GET".equals(req.getMethod())) {
-          view = rc.list();
+          viewData = new ViewData(null, rc.list());
         } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
           @SuppressWarnings("unchecked")
           AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) rc;
-          view = ac.post(rsrc);
+          viewData = new ViewData(null, ac.post(rsrc));
         } else {
           throw new MethodNotAllowedException();
         }
@@ -212,7 +220,9 @@
         IdString id = path.remove(0);
         try {
           rsrc = rc.parse(rsrc, id);
-          checkPreconditions(req, rsrc);
+          if (path.isEmpty()) {
+            checkPreconditions(req, rsrc);
+          }
         } catch (ResourceNotFoundException e) {
           if (rc instanceof AcceptsCreate
               && path.isEmpty()
@@ -220,30 +230,30 @@
                   || "PUT".equals(req.getMethod()))) {
             @SuppressWarnings("unchecked")
             AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) rc;
-            view = ac.create(rsrc, id);
+            viewData = new ViewData(null, ac.create(rsrc, id));
             status = SC_CREATED;
           } else {
             throw e;
           }
         }
-        if (view == null) {
-          view = view(rc, req.getMethod(), path);
+        if (viewData.view == null) {
+          viewData = view(rc, req.getMethod(), path);
         }
       }
-      checkAccessAnnotations(view.getClass());
+      checkRequiresCapability(viewData);
 
-      while (view instanceof RestCollection<?,?>) {
+      while (viewData.view instanceof RestCollection<?,?>) {
         @SuppressWarnings("unchecked")
         RestCollection<RestResource, RestResource> c =
-            (RestCollection<RestResource, RestResource>) view;
+            (RestCollection<RestResource, RestResource>) viewData.view;
 
         if (path.isEmpty()) {
           if ("GET".equals(req.getMethod())) {
-            view = c.list();
+            viewData = new ViewData(null, c.list());
           } else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
             @SuppressWarnings("unchecked")
             AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c;
-            view = ac.post(rsrc);
+            viewData = new ViewData(null, ac.post(rsrc));
           } else {
             throw new MethodNotAllowedException();
           }
@@ -253,7 +263,7 @@
           try {
             rsrc = c.parse(rsrc, id);
             checkPreconditions(req, rsrc);
-            view = null;
+            viewData = new ViewData(null, null);
           } catch (ResourceNotFoundException e) {
             if (c instanceof AcceptsCreate
                 && path.isEmpty()
@@ -261,53 +271,58 @@
                     || "PUT".equals(req.getMethod()))) {
               @SuppressWarnings("unchecked")
               AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
-              view = ac.create(rsrc, id);
+              viewData = new ViewData(null, ac.create(rsrc, id));
               status = SC_CREATED;
             } else {
               throw e;
             }
           }
-          if (view == null) {
-            view = view(c, req.getMethod(), path);
+          if (viewData.view == null) {
+            viewData = view(c, req.getMethod(), path);
           }
         }
-        checkAccessAnnotations(view.getClass());
+        checkRequiresCapability(viewData);
+      }
+
+      if (notModified(req, rsrc)) {
+        res.sendError(SC_NOT_MODIFIED);
+        return;
       }
 
       Multimap<String, String> config = LinkedHashMultimap.create();
       ParameterParser.splitQueryString(req.getQueryString(), config, params);
-      if (!globals.paramParser.get().parse(view, params, req, res)) {
+      if (!globals.paramParser.get().parse(viewData.view, params, req, res)) {
         return;
       }
 
-      if (view instanceof RestModifyView<?, ?>) {
+      if (viewData.view instanceof RestModifyView<?, ?>) {
         @SuppressWarnings("unchecked")
         RestModifyView<RestResource, Object> m =
-            (RestModifyView<RestResource, Object>) view;
+            (RestModifyView<RestResource, Object>) viewData.view;
 
         inputRequestBody = parseRequest(req, inputType(m));
         result = m.apply(rsrc, inputRequestBody);
-      } else if (view instanceof RestReadView<?>) {
-        result = ((RestReadView<RestResource>) view).apply(rsrc);
+      } else if (viewData.view instanceof RestReadView<?>) {
+        result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
       } else {
         throw new ResourceNotFoundException();
       }
 
       if (result instanceof Response) {
         @SuppressWarnings("rawtypes")
-        Response r = (Response) result;
+        Response<?> r = (Response) result;
         status = r.statusCode();
+        configureCaching(req, res, rsrc, r.caching());
       } else if (result instanceof Response.Redirect) {
+        CacheHeaders.setNotCacheable(res);
         res.sendRedirect(((Response.Redirect) result).location());
         return;
+      } else {
+        CacheHeaders.setNotCacheable(res);
       }
       res.setStatus(status);
 
-      if (result instanceof StreamingResponse) {
-        StreamingResponse r = (StreamingResponse) result;
-        res.setContentType(r.getContentType());
-        r.stream(res.getOutputStream());
-      } else if (result != Response.none()) {
+      if (result != Response.none()) {
         result = Response.unwrap(result);
         if (result instanceof BinaryResult) {
           replyBinaryResult(req, res, (BinaryResult) result);
@@ -316,27 +331,27 @@
         }
       }
     } catch (AuthException e) {
-      replyError(res, status = SC_FORBIDDEN, e.getMessage());
+      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching());
     } catch (BadRequestException e) {
-      replyError(res, status = SC_BAD_REQUEST, e.getMessage());
+      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching());
     } catch (MethodNotAllowedException e) {
-      replyError(res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed");
+      replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching());
     } catch (ResourceConflictException e) {
-      replyError(res, status = SC_CONFLICT, e.getMessage());
+      replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching());
     } catch (PreconditionFailedException e) {
-      replyError(res, status = SC_PRECONDITION_FAILED,
-          Objects.firstNonNull(e.getMessage(), "Precondition failed"));
+      replyError(req, res, status = SC_PRECONDITION_FAILED,
+          Objects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching());
     } catch (ResourceNotFoundException e) {
-      replyError(res, status = SC_NOT_FOUND, "Not found");
+      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
     } catch (UnprocessableEntityException e) {
-      replyError(res, status = 422,
-          Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"));
+      replyError(req, res, status = 422,
+          Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
     } catch (AmbiguousViewException e) {
-      replyError(res, status = SC_NOT_FOUND, e.getMessage());
+      replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
     } catch (MalformedJsonException e) {
-      replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
     } catch (JsonParseException e) {
-      replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
     } catch (Exception e) {
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
@@ -348,6 +363,68 @@
     }
   }
 
+  private static boolean notModified(HttpServletRequest req, RestResource rsrc) {
+    if (!"GET".equals(req.getMethod())) {
+      return false;
+    }
+
+    if (rsrc instanceof RestResource.HasETag) {
+      String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
+      if (have != null) {
+        return have.equals(((RestResource.HasETag) rsrc).getETag());
+      }
+    }
+
+    if (rsrc instanceof RestResource.HasLastModified) {
+      Timestamp m = ((RestResource.HasLastModified) rsrc).getLastModified();
+      long d = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
+
+      // HTTP times are in seconds, database may have millisecond precision.
+      return d / 1000L == m.getTime() / 1000L;
+    }
+    return false;
+  }
+
+  private static <T> void configureCaching(HttpServletRequest req,
+      HttpServletResponse res, RestResource rsrc, CacheControl c) {
+    if ("GET".equals(req.getMethod())) {
+      switch (c.getType()) {
+        case NONE:
+        default:
+          CacheHeaders.setNotCacheable(res);
+          break;
+        case PRIVATE:
+          addResourceStateHeaders(res, rsrc);
+          CacheHeaders.setCacheablePrivate(res,
+              c.getAge(), c.getUnit(),
+              c.isMustRevalidate());
+          break;
+        case PUBLIC:
+          addResourceStateHeaders(res, rsrc);
+          CacheHeaders.setCacheable(req, res,
+              c.getAge(), c.getUnit(),
+              c.isMustRevalidate());
+          break;
+      }
+    } else {
+      CacheHeaders.setNotCacheable(res);
+    }
+  }
+
+  private static void addResourceStateHeaders(
+      HttpServletResponse res, RestResource rsrc) {
+    if (rsrc instanceof RestResource.HasETag) {
+      res.setHeader(
+          HttpHeaders.ETAG,
+          ((RestResource.HasETag) rsrc).getETag());
+    }
+    if (rsrc instanceof RestResource.HasLastModified) {
+      res.setDateHeader(
+          HttpHeaders.LAST_MODIFIED,
+          ((RestResource.HasLastModified) rsrc).getLastModified().getTime());
+    }
+  }
+
   private void checkPreconditions(HttpServletRequest req, RestResource rsrc)
       throws PreconditionFailedException {
     if ("*".equals(req.getHeader("If-None-Match"))) {
@@ -414,8 +491,9 @@
       } finally {
         br.close();
       }
-    } else if ("PUT".equals(req.getMethod()) && acceptsPutInput(type)) {
-      return parsePutInput(req, type);
+    } else if (("PUT".equals(req.getMethod()) || "POST".equals(req.getMethod()))
+        && acceptsRawInput(type)) {
+      return parseRawInput(req, type);
     } else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
       return null;
     } else if (hasNoBody(req)) {
@@ -451,10 +529,10 @@
   }
 
   @SuppressWarnings("rawtypes")
-  private static boolean acceptsPutInput(Type type) {
+  private static boolean acceptsRawInput(Type type) {
     if (type instanceof Class) {
       for (Field f : ((Class) type).getDeclaredFields()) {
-        if (f.getType() == PutInput.class) {
+        if (f.getType() == RawInput.class) {
           return true;
         }
       }
@@ -462,15 +540,15 @@
     return false;
   }
 
-  private Object parsePutInput(final HttpServletRequest req, Type type)
+  private Object parseRawInput(final HttpServletRequest req, Type type)
       throws SecurityException, NoSuchMethodException,
       IllegalArgumentException, InstantiationException, IllegalAccessException,
       InvocationTargetException, MethodNotAllowedException {
     Object obj = createInstance(type);
     for (Field f : obj.getClass().getDeclaredFields()) {
-      if (f.getType() == PutInput.class) {
+      if (f.getType() == RawInput.class) {
         f.setAccessible(true);
-        f.set(obj, new PutInput() {
+        f.set(obj, new RawInput() {
           @Override
           public String getContentType() {
             return req.getContentType();
@@ -534,7 +612,7 @@
       Multimap<String, String> config,
       Object result)
       throws IOException {
-    final TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
+    TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
     buf.write(JSON_MAGIC);
     Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
     Gson gson = newGson(config, req);
@@ -545,18 +623,9 @@
     }
     w.write('\n');
     w.flush();
-
-    replyBinaryResult(req, res, new BinaryResult() {
-      @Override
-      public long getContentLength() {
-        return buf.length();
-      }
-
-      @Override
-      public void writeTo(OutputStream os) throws IOException {
-        buf.writeTo(os, null);
-      }
-    }.setContentType(JSON_TYPE).setCharacterEncoding(UTF_8.name()));
+    replyBinaryResult(req, res, asBinaryResult(buf)
+      .setContentType(JSON_TYPE)
+      .setCharacterEncoding(UTF_8.name()));
   }
 
   private static Gson newGson(Multimap<String, String> config,
@@ -626,47 +695,86 @@
       @Nullable HttpServletRequest req,
       HttpServletResponse res,
       BinaryResult bin) throws IOException {
+    final BinaryResult appResult = bin;
     try {
+      if (bin.getAttachmentName() != null) {
+        res.setHeader(
+            "Content-Disposition",
+            "attachment; filename=\"" + bin.getAttachmentName() + "\"");
+      }
+      if (bin.isBase64()) {
+        bin = stackBase64(res, bin);
+      }
+      if (bin.canGzip() && acceptsGzip(req)) {
+        bin = stackGzip(res, bin);
+      }
+
       res.setContentType(bin.getContentType());
+      long len = bin.getContentLength();
+      if (0 <= len && len < Integer.MAX_VALUE) {
+        res.setContentLength((int) len);
+      } else if (0 <= len) {
+        res.setHeader("Content-Length", Long.toString(len));
+      }
+
       OutputStream dst = res.getOutputStream();
       try {
-        long len = bin.getContentLength();
-        boolean gzip = bin.canGzip() && acceptsGzip(req);
-        if (gzip && 256 <= len && len <= (10 << 20)) {
-          TemporaryBuffer.Heap buf = compress(bin);
-          if (buf.length() < len) {
-            res.setContentLength((int) buf.length());
-            res.setHeader("Content-Encoding", "gzip");
-            buf.writeTo(dst, null);
-          } else {
-            replyUncompressed(res, dst, bin, len);
-          }
-        } else if (gzip) {
-          res.setHeader("Content-Encoding", "gzip");
-          dst = new GZIPOutputStream(dst);
-          bin.writeTo(dst);
-        } else {
-          replyUncompressed(res, dst, bin, len);
-        }
+        bin.writeTo(dst);
       } finally {
         dst.close();
       }
     } finally {
-      bin.close();
+      appResult.close();
     }
   }
 
-  private static void replyUncompressed(HttpServletResponse res,
-      OutputStream dst, BinaryResult bin, long len) throws IOException {
-    if (0 <= len && len < Integer.MAX_VALUE) {
-      res.setContentLength((int) len);
-    } else if (0 <= len) {
-      res.setHeader("Content-Length", Long.toString(len));
+  private static BinaryResult stackBase64(HttpServletResponse res,
+      final BinaryResult src) throws IOException {
+    BinaryResult b64;
+    long len = src.getContentLength();
+    if (0 <= len && len <= (7 << 20)) {
+      b64 = base64(src);
+    } else {
+      b64 = new BinaryResult() {
+        @Override
+        public void writeTo(OutputStream out) throws IOException {
+          OutputStream e = BaseEncoding.base64().encodingStream(
+              new OutputStreamWriter(out, Charsets.ISO_8859_1));
+          src.writeTo(e);
+          e.flush();
+        }
+      };
     }
-    bin.writeTo(dst);
+    res.setHeader("X-FYI-Content-Encoding", "base64");
+    res.setHeader("X-FYI-Content-Type", src.getContentType());
+    return b64.setContentType("text/plain").setCharacterEncoding("ISO-8859-1");
   }
 
-  private RestView<RestResource> view(
+  private static BinaryResult stackGzip(HttpServletResponse res,
+      final BinaryResult src) throws IOException {
+    BinaryResult gz;
+    long len = src.getContentLength();
+    if (256 <= len && len <= (10 << 20)) {
+      gz = compress(src);
+      if (len <= gz.getContentLength()) {
+        return src;
+      }
+    } else {
+      gz = new BinaryResult() {
+        @Override
+        public void writeTo(OutputStream out) throws IOException {
+          GZIPOutputStream gz = new GZIPOutputStream(out);
+          src.writeTo(gz);
+          gz.finish();
+          gz.flush();
+        }
+      };
+    }
+    res.setHeader("Content-Encoding", "gzip");
+    return gz.setContentType(src.getContentType());
+  }
+
+  private ViewData view(
       RestCollection<RestResource, RestResource> rc,
       String method, List<IdString> path) throws ResourceNotFoundException,
       MethodNotAllowedException, AmbiguousViewException {
@@ -683,10 +791,14 @@
 
     List<String> p = splitProjection(projection);
     if (p.size() == 2) {
+      String viewname = p.get(1);
+      if (Strings.isNullOrEmpty(viewname)) {
+        viewname = "/";
+      }
       RestView<RestResource> view =
-          views.get(p.get(0), method + "." + p.get(1));
+          views.get(p.get(0), method + "." + viewname);
       if (view != null) {
-        return view;
+        return new ViewData(p.get(0), view);
       }
       throw new ResourceNotFoundException(projection);
     }
@@ -694,7 +806,7 @@
     String name = method + "." + p.get(0);
     RestView<RestResource> core = views.get("gerrit", name);
     if (core != null) {
-      return core;
+      return new ViewData(null, core);
     }
 
     Map<String, RestView<RestResource>> r = Maps.newTreeMap();
@@ -706,12 +818,14 @@
     }
 
     if (r.size() == 1) {
-      return Iterables.getFirst(r.values(), null);
+      Map.Entry<String, RestView<RestResource>> entry =
+          Iterables.getOnlyElement(r.entrySet());
+      return new ViewData(entry.getKey(), entry.getValue());
     } else if (r.isEmpty()) {
       throw new ResourceNotFoundException(projection);
     } else {
       throw new AmbiguousViewException(String.format(
-        "Projection %s is ambiguous: ",
+        "Projection %s is ambiguous: %s",
         name,
         Joiner.on(", ").join(
           Iterables.transform(r.keySet(), new Function<String, String>() {
@@ -762,18 +876,9 @@
     return !("GET".equals(method) || "HEAD".equals(method));
   }
 
-  private void checkAccessAnnotations(Class<? extends Object> clazz)
-      throws AuthException {
-    RequiresCapability rc = clazz.getAnnotation(RequiresCapability.class);
-    if (rc != null) {
-      CurrentUser user = globals.currentUser.get();
-      CapabilityControl ctl = user.getCapabilities();
-      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
-        throw new AuthException(String.format(
-            "Capability %s is required to access this resource",
-            rc.value()));
-      }
-    }
+  private void checkRequiresCapability(ViewData viewData) throws AuthException {
+    CapabilityUtils.checkRequiresCapability(globals.currentUser,
+        viewData.pluginName, viewData.view.getClass());
   }
 
   private static void handleException(Throwable err, HttpServletRequest req,
@@ -786,13 +891,20 @@
 
     if (!res.isCommitted()) {
       res.reset();
-      replyError(res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
+      replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
     }
   }
 
-  static void replyError(HttpServletResponse res, int statusCode, String msg)
-      throws IOException {
+  public static void replyError(HttpServletRequest req,
+      HttpServletResponse res, int statusCode, String msg) throws IOException {
+    replyError(req, res, statusCode, msg, CacheControl.NONE);
+  }
+
+  public static void replyError(HttpServletRequest req,
+      HttpServletResponse res, int statusCode, String msg,
+      CacheControl c) throws IOException {
     res.setStatus(statusCode);
+    configureCaching(req, res, null, c);
     replyText(null, res, msg);
   }
 
@@ -842,14 +954,33 @@
     return false;
   }
 
-  private static TemporaryBuffer.Heap compress(BinaryResult bin)
+  private static BinaryResult base64(BinaryResult bin)
+      throws IOException {
+    int max = 4 * IntMath.divide((int) bin.getContentLength(), 3, CEILING);
+    TemporaryBuffer.Heap buf = heap(max);
+    OutputStream encoded = BaseEncoding.base64().encodingStream(
+        new OutputStreamWriter(buf, Charsets.ISO_8859_1));
+    bin.writeTo(encoded);
+    encoded.close();
+    return asBinaryResult(buf);
+  }
+
+  private static BinaryResult compress(BinaryResult bin)
       throws IOException {
     TemporaryBuffer.Heap buf = heap(20 << 20);
     GZIPOutputStream gz = new GZIPOutputStream(buf);
     bin.writeTo(gz);
-    gz.finish();
-    gz.flush();
-    return buf;
+    gz.close();
+    return asBinaryResult(buf).setContentType(bin.getContentType());
+  }
+
+  private static BinaryResult asBinaryResult(final TemporaryBuffer.Heap buf) {
+    return new BinaryResult() {
+      @Override
+      public void writeTo(OutputStream os) throws IOException {
+        buf.writeTo(os, null);
+      }
+    }.setContentLength(buf.length());
   }
 
   private static Heap heap(int max) {
@@ -862,4 +993,14 @@
       super(message);
     }
   }
+
+  private static class ViewData {
+    String pluginName;
+    RestView<RestResource> view;
+
+    ViewData(String pluginName, RestView<RestResource> view) {
+      this.pluginName = pluginName;
+      this.view = view;
+    }
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
index 059b54c..c0c4535 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
@@ -28,7 +28,7 @@
     super(response);
   }
 
-  int getStatus() {
+  public int getStatus() {
     return status;
   }
 
@@ -39,6 +39,7 @@
   }
 
   @Override
+  @Deprecated
   public void setStatus(int sc, String sm) {
     super.setStatus(sc, sm);
     this.status = sc;
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 3277992..940e2e6 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
@@ -42,7 +42,7 @@
 
   protected Account.Id getAccountId() {
     CurrentUser u = currentUser.get();
-    if (u instanceof IdentifiedUser) {
+    if (u.isIdentifiedUser()) {
       return ((IdentifiedUser) u).getAccountId();
     }
     return null;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
deleted file mode 100644
index 0b54db1..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc;
-
-import com.google.gerrit.common.data.ChangeListService;
-import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-public class ChangeListServiceImpl extends BaseServiceImplementation implements
-    ChangeListService {
-  private final Provider<CurrentUser> currentUser;
-
-  @Inject
-  ChangeListServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<CurrentUser> currentUser) {
-    super(schema, currentUser);
-    this.currentUser = currentUser;
-  }
-
-  public void toggleStars(final ToggleStarRequest req,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException {
-        final Account.Id me = getAccountId();
-        final Set<Change.Id> existing = currentUser.get().getStarredChanges();
-        List<StarredChange> add = new ArrayList<StarredChange>();
-        List<StarredChange.Key> remove = new ArrayList<StarredChange.Key>();
-
-        if (req.getAddSet() != null) {
-          for (final Change.Id id : req.getAddSet()) {
-            if (!existing.contains(id)) {
-              add.add(new StarredChange(new StarredChange.Key(me, id)));
-            }
-          }
-        }
-
-        if (req.getRemoveSet() != null) {
-          for (final Change.Id id : req.getRemoveSet()) {
-            remove.add(new StarredChange.Key(me, id));
-          }
-        }
-
-        db.starredChanges().insert(add);
-        db.starredChanges().deleteKeys(remove);
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-}
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 55ecb01..5a0d328 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gson.GsonBuilder;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.server.ActiveCall;
@@ -238,7 +239,7 @@
         final HttpServletResponse o) {
       super(i, o);
       this.session = session;
-      this.when = System.currentTimeMillis();
+      this.when = TimeUtil.nowMs();
     }
 
     @Override
@@ -290,7 +291,7 @@
     }
 
     public long getElapsed() {
-      return System.currentTimeMillis() - when;
+      return TimeUtil.nowMs() - when;
     }
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index 22546a7..18cce28 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.ReviewerInfo;
@@ -46,6 +47,7 @@
 
 import org.eclipse.jgit.lib.Config;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -53,8 +55,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import javax.annotation.Nullable;
-
 class SuggestServiceImpl extends BaseServiceImplementation implements
     SuggestService {
   private static final String MAX_SUFFIX = "\u9fa5";
@@ -218,7 +218,8 @@
       final String query, final int limit,
       final AsyncCallback<List<ReviewerInfo>> callback) {
     run(callback, new Action<List<ReviewerInfo>>() {
-      public List<ReviewerInfo> run(final ReviewDb db) throws OrmException {
+      public List<ReviewerInfo> run(final ReviewDb db)
+          throws OrmException, Failure {
         final ChangeControl changeControl;
         try {
           changeControl = changeControlFactory.controlFor(change);
@@ -273,7 +274,7 @@
   }
 
   private boolean suggestGroupAsReviewer(final Project.NameKey project,
-      final GroupReference group) throws OrmException {
+      final GroupReference group) throws OrmException, Failure {
     if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
       return false;
     }
@@ -296,6 +297,8 @@
       return false;
     } catch (NoSuchProjectException e) {
       return false;
+    } catch (IOException e) {
+      throw new Failure(e);
     }
 
     return true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
index 7de332a..08e1582 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
@@ -27,7 +27,6 @@
 
   @Override
   protected void configureServlets() {
-    rpc(ChangeListServiceImpl.class);
     rpc(SuggestServiceImpl.class);
     rpc(SystemInfoServiceImpl.class);
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/access/AccessRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/access/AccessRestApiServlet.java
new file mode 100644
index 0000000..fda6416
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/access/AccessRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.access;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.access.AccessCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AccessRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  AccessRestApiServlet(RestApiServlet.Globals globals,
+      Provider<AccessCollection> access) {
+    super(globals, access);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index bef6316..d0fb504 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.httpd.rpc.RpcServletModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
 import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.mail.RegisterNewEmailSender;
 
 public class AccountModule extends RpcServletModule {
   public AccountModule() {
@@ -32,7 +31,6 @@
         factory(AgreementInfoFactory.Factory.class);
         factory(DeleteExternalIds.Factory.class);
         factory(ExternalIdDetailFactory.Factory.class);
-        factory(RegisterNewEmailSender.Factory.class);
       }
     });
     rpc(AccountSecurityImpl.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 6d183b8..b6549ea 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -19,8 +19,6 @@
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
@@ -30,8 +28,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -40,49 +36,35 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.ChangeUserName;
-import com.google.gerrit.server.account.ClearPassword;
-import com.google.gerrit.server.account.GeneratePassword;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
-import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
 class AccountSecurityImpl extends BaseServiceImplementation implements
     AccountSecurity {
-  private final Logger log = LoggerFactory.getLogger(getClass());
   private final ContactStore contactStore;
-  private final AuthConfig authConfig;
   private final Realm realm;
   private final ProjectCache projectCache;
   private final Provider<IdentifiedUser> user;
   private final EmailTokenVerifier emailTokenVerifier;
-  private final RegisterNewEmailSender.Factory registerNewEmailFactory;
-  private final SshKeyCache sshKeyCache;
   private final AccountByEmailCache byEmailCache;
   private final AccountCache accountCache;
   private final AccountManager accountManager;
   private final boolean useContactInfo;
 
-  private final ClearPassword.Factory clearPasswordFactory;
-  private final GeneratePassword.Factory generatePasswordFactory;
   private final ChangeUserName.CurrentUser changeUserNameFactory;
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
@@ -93,34 +75,26 @@
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser, final ContactStore cs,
-      final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
+      final Realm r, final Provider<IdentifiedUser> u,
       final EmailTokenVerifier etv, final ProjectCache pc,
-      final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
       final AccountByEmailCache abec, final AccountCache uac,
       final AccountManager am,
-      final ClearPassword.Factory clearPasswordFactory,
-      final GeneratePassword.Factory generatePasswordFactory,
       final ChangeUserName.CurrentUser changeUserNameFactory,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
       final ChangeHooks hooks, final GroupCache groupCache) {
     super(schema, currentUser);
     contactStore = cs;
-    authConfig = ac;
     realm = r;
     user = u;
     emailTokenVerifier = etv;
     projectCache = pc;
-    registerNewEmailFactory = esf;
-    sshKeyCache = skc;
     byEmailCache = abec;
     accountCache = uac;
     accountManager = am;
 
     useContactInfo = contactStore != null && contactStore.isEnabled();
 
-    this.clearPasswordFactory = clearPasswordFactory;
-    this.generatePasswordFactory = generatePasswordFactory;
     this.changeUserNameFactory = changeUserNameFactory;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
@@ -128,60 +102,6 @@
     this.groupCache = groupCache;
   }
 
-  public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
-    run(callback, new Action<List<AccountSshKey>>() {
-      public List<AccountSshKey> run(ReviewDb db) throws OrmException {
-        IdentifiedUser u = user.get();
-        return db.accountSshKeys().byAccount(u.getAccountId()).toList();
-      }
-    });
-  }
-
-  public void addSshKey(final String keyText,
-      final AsyncCallback<AccountSshKey> callback) {
-    run(callback, new Action<AccountSshKey>() {
-      public AccountSshKey run(final ReviewDb db) throws OrmException, Failure {
-        int max = 0;
-        final Account.Id me = user.get().getAccountId();
-        for (final AccountSshKey k : db.accountSshKeys().byAccount(me)) {
-          max = Math.max(max, k.getKey().get());
-        }
-
-        final AccountSshKey key;
-        try {
-          key = sshKeyCache.create(new AccountSshKey.Id(me, max + 1), keyText);
-        } catch (InvalidSshKeyException e) {
-          throw new Failure(e);
-        }
-        db.accountSshKeys().insert(Collections.singleton(key));
-        uncacheSshKeys();
-        return key;
-      }
-    });
-  }
-
-  public void deleteSshKeys(final Set<AccountSshKey.Id> ids,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final Account.Id me = user.get().getAccountId();
-        for (final AccountSshKey.Id keyId : ids) {
-          if (!me.equals(keyId.getParentKey()))
-            throw new Failure(new NoSuchEntityException());
-        }
-
-        db.accountSshKeys().deleteKeys(ids);
-        uncacheSshKeys();
-
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  private void uncacheSshKeys() {
-    sshKeyCache.evict(user.get().getUserName());
-  }
-
   @Override
   public void changeUserName(final String newName,
       final AsyncCallback<VoidResult> callback) {
@@ -193,18 +113,6 @@
     }
   }
 
-  @Override
-  public void generatePassword(AccountExternalId.Key key,
-      AsyncCallback<AccountExternalId> callback) {
-    Handler.wrap(generatePasswordFactory.create(key)).to(callback);
-  }
-
-  @Override
-  public void clearPassword(AccountExternalId.Key key,
-      AsyncCallback<AccountExternalId> callback) {
-    Handler.wrap(clearPasswordFactory.create(key)).to(callback);
-  }
-
   public void myExternalIds(AsyncCallback<List<AccountExternalId>> callback) {
     externalIdDetailFactory.create().to(callback);
   }
@@ -232,7 +140,7 @@
         if (useContactInfo) {
           if (ContactInformation.hasAddress(info)
               || (me.isContactFiled() && ContactInformation.hasData(info))) {
-            me.setContactFiled();
+            me.setContactFiled(TimeUtil.nowTs());
           }
           if (ContactInformation.hasData(info)) {
             try {
@@ -291,8 +199,8 @@
         if (m == null) {
           m = new AccountGroupMember(key);
           db.accountGroupMembersAudit().insert(
-              Collections.singleton(
-                  new AccountGroupMemberAudit(m, account.getId())));
+              Collections.singleton(new AccountGroupMemberAudit(
+                  m, account.getId(), TimeUtil.nowTs())));
           db.accountGroupMembers().insert(Collections.singleton(m));
           accountCache.evict(m.getAccountId());
         }
@@ -302,31 +210,6 @@
     });
   }
 
-  public void registerEmail(final String address,
-      final AsyncCallback<Account> cb) {
-    if (authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
-      try {
-        accountManager.link(user.get().getAccountId(),
-            AuthRequest.forEmail(address));
-        cb.onSuccess(user.get().getAccount());
-      } catch (AccountException e) {
-        cb.onFailure(e);
-      }
-    } else {
-      try {
-        final RegisterNewEmailSender sender;
-        sender = registerNewEmailFactory.create(address);
-        sender.send();
-      } catch (EmailException e) {
-        log.error("Cannot send email verification message to " + address, e);
-        cb.onFailure(e);
-      } catch (RuntimeException e) {
-        log.error("Cannot send email verification message to " + address, e);
-        cb.onFailure(e);
-      }
-    }
-  }
-
   public void validateEmail(final String tokenString,
       final AsyncCallback<VoidResult> callback) {
     try {
@@ -342,6 +225,8 @@
       callback.onFailure(e);
     } catch (AccountException e) {
       callback.onFailure(e);
+    } catch (OrmException e) {
+      callback.onFailure(e);
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 2fe3124..0a9ed28 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
@@ -154,7 +154,7 @@
         if (filter != null) {
           try {
             ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
-            builder.setAllowFile(true);
+            builder.setAllowFileRegex(true);
             builder.parse(filter);
           } catch (QueryParseException badFilter) {
             throw new InvalidQueryException(badFilter.getMessage(), filter);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 557e017..cf8b4db 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -18,6 +18,9 @@
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+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.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,15 +30,16 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.Mergeable;
+import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -48,6 +52,8 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -61,6 +67,9 @@
 
 /** Creates a {@link ChangeDetail} from a {@link Change}. */
 public class ChangeDetailFactory extends Handler<ChangeDetail> {
+  private static final Logger log = LoggerFactory
+      .getLogger(ChangeDetailFactory.class);
+
   public interface Factory {
     ChangeDetailFactory create(Change.Id id);
   }
@@ -78,7 +87,7 @@
   private ChangeControl control;
   private Map<PatchSet.Id, PatchSet> patchsetsById;
 
-  private final MergeOp.Factory opFactory;
+  private final Mergeable mergeable;
   private boolean testMerge;
 
   private List<PatchSetAncestor> currentPatchSetAncestors;
@@ -92,7 +101,7 @@
       final ChangeControl.Factory changeControlFactory,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final AnonymousUser anonymousUser,
-      final MergeOp.Factory opFactory,
+      final Mergeable mergeable,
       @GerritServerConfig final Config cfg,
       @Assisted final Change.Id id) {
     this.patchSetDetail = patchSetDetail;
@@ -102,7 +111,7 @@
     this.anonymousUser = anonymousUser;
     this.aic = accountInfoCacheFactory.create();
 
-    this.opFactory = opFactory;
+    this.mergeable = mergeable;
     this.testMerge = cfg.getBoolean("changeMerge", "test", false);
 
     this.changeId = id;
@@ -135,7 +144,7 @@
         changeId));
 
     detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
-
+    detail.setCanCherryPick(control.getProjectControl().canUpload());
     detail.setCanEdit(control.getRefControl().canWrite());
     detail.setCanEditCommitMessage(change.getStatus().isOpen() && control.canAddPatchSet());
     detail.setCanEditTopicName(control.canEditTopicName());
@@ -180,7 +189,7 @@
     Set<PatchSet.Id> patchesWithDraftComments = new HashSet<PatchSet.Id>();
     final CurrentUser user = control.getCurrentUser();
     final Account.Id me =
-        user instanceof IdentifiedUser ? ((IdentifiedUser) user).getAccountId()
+        user.isIdentifiedUser() ? ((IdentifiedUser) user).getAccountId()
             : null;
     for (PatchSet ps : source) {
       final PatchSet.Id psId = ps.getId();
@@ -205,7 +214,7 @@
       PatchSet.Id id = msg.getPatchSetId();
       if (id != null) {
         PatchSet ps = patchsetsById.get(msg.getPatchSetId());
-        if (control.isPatchVisible(ps, db)) {
+        if (ps != null && control.isPatchVisible(ps, db)) {
           msgList.add(msg);
         }
       } else {
@@ -224,7 +233,21 @@
     final Change.Status status = detail.getChange().getStatus();
     if ((status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) &&
         testMerge) {
-      ChangeUtil.testMerge(opFactory, detail.getChange());
+      try {
+        detail.getChange().setMergeable(mergeable.apply(new RevisionResource(
+            new ChangeResource(control),
+            detail.getCurrentPatchSet())).mergeable);
+      } catch (RepositoryNotFoundException e) {
+        log.warn("Cannot check mergeable", e);
+      } catch (ResourceConflictException e) {
+        log.warn("Cannot check mergeable", e);
+      } catch (BadRequestException e) {
+        log.warn("Cannot check mergeable", e);
+      } catch (AuthException e) {
+        log.warn("Cannot check mergeable", e);
+      } catch (IOException e) {
+        log.warn("Cannot check mergeable", e);
+      }
     }
   }
 
@@ -283,7 +306,7 @@
 
     final CurrentUser currentUser = control.getCurrentUser();
     Account.Id currentUserId = null;
-    if (currentUser instanceof IdentifiedUser) {
+    if (currentUser.isIdentifiedUser()) {
         currentUserId = ((IdentifiedUser) currentUser).getAccountId();
     }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
index 63c22ce..0f088c9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -39,6 +40,7 @@
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
+  private final ChangeIndexer indexer;
 
   private final PatchSet.Id patchSetId;
 
@@ -47,11 +49,13 @@
       final ChangeControl.Factory changeControlFactory,
       final GitRepositoryManager gitManager,
       final GitReferenceUpdated gitRefUpdated,
+      final ChangeIndexer indexer,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
+    this.indexer = indexer;
 
     this.patchSetId = patchSetId;
   }
@@ -65,7 +69,8 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db);
+    ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db,
+        indexer);
     return VoidResult.INSTANCE;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
index ba50417..564c136 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
@@ -25,18 +25,14 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CommitMessageEditedSender;
-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.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -49,8 +45,6 @@
 
 import java.io.IOException;
 
-import javax.annotation.Nullable;
-
 class EditCommitMessageHandler extends Handler<ChangeDetail> {
   interface Factory {
     EditCommitMessageHandler create(PatchSet.Id patchSetId, String message);
@@ -61,21 +55,12 @@
   private final IdentifiedUser currentUser;
   private final ChangeDetailFactory.Factory changeDetailFactory;
   private final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory;
-
-  private final GitReferenceUpdated gitRefUpdated;
-
   private final PatchSet.Id patchSetId;
   @Nullable
   private final String message;
-
-  private final ChangeHooks hooks;
-  private final CommitValidators.Factory commitValidatorsFactory;
-
   private final GitRepositoryManager gitManager;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-
   private final PersonIdent myIdent;
-  private final TrackingFooters trackingFooters;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
 
   @Inject
   EditCommitMessageHandler(final ChangeControl.Factory changeControlFactory,
@@ -83,29 +68,20 @@
       final ChangeDetailFactory.Factory changeDetailFactory,
       final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
       @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message, final ChangeHooks hooks,
-      final CommitValidators.Factory commitValidatorsFactory,
+      @Assisted @Nullable final String message,
       final GitRepositoryManager gitManager,
-      final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated gitRefUpdated,
       @GerritPersonIdent final PersonIdent myIdent,
-      TrackingFooters trackingFooters) {
+      final PatchSetInserter.Factory patchSetInserterFactory) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.currentUser = currentUser;
     this.changeDetailFactory = changeDetailFactory;
     this.commitMessageEditedSenderFactory = commitMessageEditedSenderFactory;
-
     this.patchSetId = patchSetId;
     this.message = message;
-    this.hooks = hooks;
-    this.commitValidatorsFactory = commitValidatorsFactory;
     this.gitManager = gitManager;
-
-    this.patchSetInfoFactory = patchSetInfoFactory;
-    this.gitRefUpdated = gitRefUpdated;
     this.myIdent = myIdent;
-    this.trackingFooters = trackingFooters;
+    this.patchSetInserterFactory = patchSetInserterFactory;
   }
 
   @Override
@@ -128,13 +104,9 @@
       throw new NoSuchChangeException(changeId, e);
     }
     try {
-      CommitValidators commitValidators =
-          commitValidatorsFactory.create(control.getRefControl(), new NoSshInfo(), git);
-
-      ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
-          commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, gitRefUpdated, myIdent,
-          trackingFooters);
-
+      ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(),
+          currentUser, message, db, commitMessageEditedSenderFactory, git,
+          myIdent, patchSetInserterFactory);
       return changeDetailFactory.create(changeId).call();
     } finally {
       git.close();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
index c07ee51..0c7df3e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -29,23 +30,15 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 
 /** Creates a {@link IncludedInDetail} of a {@link Change}. */
 class IncludedInDetailFactory extends Handler<IncludedInDetail> {
-  private static final Logger log =
-      LoggerFactory.getLogger(IncludedInDetailFactory.class);
 
   interface Factory {
     IncludedInDetailFactory create(Change.Id id);
@@ -56,7 +49,6 @@
   private final GitRepositoryManager repoManager;
   private final Change.Id changeId;
 
-  private IncludedInDetail detail;
   private ChangeControl control;
 
   @Inject
@@ -92,11 +84,7 @@
           throw new InvalidRevisionException();
         }
 
-        detail = new IncludedInDetail();
-        detail.setBranches(includedIn(repo, rw, rev, Constants.R_HEADS));
-        detail.setTags(includedIn(repo, rw, rev, Constants.R_TAGS));
-
-        return detail;
+        return IncludedInResolver.resolve(repo, rw, rev);
       } finally {
         rw.release();
       }
@@ -104,32 +92,4 @@
       repo.close();
     }
   }
-
-  private List<String> includedIn(final Repository repo, final RevWalk rw,
-      final RevCommit rev, final String namespace) throws IOException,
-      MissingObjectException, IncorrectObjectTypeException {
-    final List<String> result = new ArrayList<String>();
-    for (final Ref ref : repo.getRefDatabase().getRefs(namespace).values()) {
-      final RevCommit tip;
-      try {
-        tip = rw.parseCommit(ref.getObjectId());
-      } catch (IncorrectObjectTypeException notCommit) {
-        // Its OK for a tag reference to point to a blob or a tree, this
-        // is common in the Linux kernel or git.git repository.
-        //
-        continue;
-      } catch (MissingObjectException notHere) {
-        // Log the problem with this branch, but keep processing.
-        //
-        log.warn("Reference " + ref.getName() + " in " + repo.getDirectory()
-            + " points to dangling object " + ref.getObjectId());
-        continue;
-      }
-
-      if (rw.isMergedInto(rev, tip)) {
-        result.add(ref.getName().substring(namespace.length()));
-      }
-    }
-    return result;
-  }
-}
+}
\ No newline at end of file
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 8e81dd3..788c6f6 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,20 +14,30 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
+import com.google.common.base.Function;
+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.webui.UiAction;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.AccountPatchReview;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListKey;
@@ -39,6 +49,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
@@ -48,8 +59,6 @@
 import java.util.List;
 import java.util.Map;
 
-import javax.annotation.Nullable;
-
 /** Creates a {@link PatchSetDetail} from a {@link PatchSet}. */
 class PatchSetDetailFactory extends Handler<PatchSetDetail> {
 
@@ -67,6 +76,7 @@
   private final ReviewDb db;
   private final PatchListCache patchListCache;
   private final ChangeControl.Factory changeControlFactory;
+  private final Revisions revisions;
 
   private Project.NameKey projectKey;
   private final PatchSet.Id psIdBase;
@@ -83,6 +93,7 @@
   PatchSetDetailFactory(final PatchSetInfoFactory psif, final ReviewDb db,
       final PatchListCache patchListCache,
       final ChangeControl.Factory changeControlFactory,
+      final Revisions revisions,
       @Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
       @Assisted("psIdNew") final PatchSet.Id psIdNew,
       @Assisted @Nullable final AccountDiffPreference diffPrefs) {
@@ -90,6 +101,7 @@
     this.db = db;
     this.patchListCache = patchListCache;
     this.changeControlFactory = changeControlFactory;
+    this.revisions = revisions;
 
     this.psIdBase = psIdBase;
     this.psIdNew = psIdNew;
@@ -143,7 +155,7 @@
     detail.setPatches(patches);
 
     final CurrentUser user = control.getCurrentUser();
-    if (user instanceof IdentifiedUser) {
+    if (user.isIdentifiedUser()) {
       // 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.
@@ -164,6 +176,23 @@
       }
     }
 
+    detail.setCommands(Lists.newArrayList(Iterables.transform(
+        UiActions.sorted(UiActions.plugins(UiActions.from(
+          revisions,
+          new RevisionResource(new ChangeResource(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/changedetail/RebaseChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
index b9acfa9..8558fe8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
@@ -62,7 +62,7 @@
       EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, NoSuchProjectException {
-    rebaseChange.rebase(patchSetId, currentUser.getAccountId());
+    rebaseChange.rebase(patchSetId, currentUser);
     return changeDetailFactory.create(patchSetId.getParentKey()).call();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/config/ConfigRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/config/ConfigRestApiServlet.java
new file mode 100644
index 0000000..f951ad3
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/config/ConfigRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.config;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.config.ConfigCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ConfigRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  ConfigRestApiServlet(RestApiServlet.Globals globals,
+      Provider<ConfigCollection> configCollection) {
+    super(globals, configCollection);
+  }
+}
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 3174757..35a2b3d 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.ReviewResult;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
+import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.httpd.rpc.changedetail.ChangeDetailFactory;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Change;
@@ -29,7 +30,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
+import com.google.gerrit.server.patch.PatchScriptFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -49,6 +52,7 @@
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
   private final SaveDraft.Factory saveDraftFactory;
   private final ChangeDetailFactory.Factory changeDetailFactory;
+  private final ChangeControl.Factory changeControlFactory;
 
   @Inject
   PatchDetailServiceImpl(final Provider<ReviewDb> schema,
@@ -56,13 +60,15 @@
       final DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory,
       final PatchScriptFactory.Factory patchScriptFactoryFactory,
       final SaveDraft.Factory saveDraftFactory,
-      final ChangeDetailFactory.Factory changeDetailFactory) {
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      final ChangeControl.Factory changeControlFactory) {
     super(schema, currentUser);
 
     this.deleteDraftPatchSetFactory = deleteDraftPatchSetFactory;
     this.patchScriptFactoryFactory = patchScriptFactoryFactory;
     this.saveDraftFactory = saveDraftFactory;
     this.changeDetailFactory = changeDetailFactory;
+    this.changeControlFactory = changeControlFactory;
   }
 
   public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
@@ -72,7 +78,16 @@
       callback.onFailure(new NoSuchEntityException());
       return;
     }
-    patchScriptFactoryFactory.create(patchKey, psa, psb, dp).to(callback);
+
+    new Handler<PatchScript>() {
+      @Override
+      public PatchScript call() throws Exception {
+        Change.Id changeId = patchKey.getParentKey().getParentKey();
+        ChangeControl control = changeControlFactory.validateFor(changeId);
+        return patchScriptFactoryFactory.create(
+            control, patchKey.getFileName(), psa, psb, dp).call();
+      }
+    }.to(callback);
   }
 
   public void saveDraft(final PatchLineComment comment,
@@ -124,7 +139,7 @@
           }
           return changeDetailFactory.create(result.getChangeId()).call();
         } catch (NoSuchChangeException e) {
-          throw new Failure(new NoSuchChangeException(result.getChangeId()));
+          throw new Failure(new NoSuchChangeException(psid.getParentKey()));
         } catch (NoSuchProjectException e) {
           throw new Failure(e);
         } catch (NoSuchEntityException e) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
index d1f5b24..4e69b14 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
@@ -28,11 +28,9 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(PatchScriptFactory.Factory.class);
         factory(SaveDraft.Factory.class);
       }
     });
-    bind(PatchScriptBuilder.class);
     rpc(PatchDetailServiceImpl.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
deleted file mode 100644
index 9a4b89f..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ /dev/null
@@ -1,523 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.patch;
-
-import com.google.gerrit.common.data.CommentDetail;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchScript.DisplayMethod;
-import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.FileTypeRegistry;
-import com.google.gerrit.server.patch.IntraLineDiff;
-import com.google.gerrit.server.patch.IntraLineDiffKey;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.Text;
-import com.google.inject.Inject;
-
-import eu.medsea.mimeutil.MimeType;
-import eu.medsea.mimeutil.MimeUtil2;
-
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-class PatchScriptBuilder {
-  static final int MAX_CONTEXT = 5000000;
-  static final int BIG_FILE = 9000;
-
-  private static final Comparator<Edit> EDIT_SORT = new Comparator<Edit>() {
-    @Override
-    public int compare(final Edit o1, final Edit o2) {
-      return o1.getBeginA() - o2.getBeginA();
-    }
-  };
-
-  private Repository db;
-  private Project.NameKey projectKey;
-  private ObjectReader reader;
-  private Change change;
-  private AccountDiffPreference diffPrefs;
-  private boolean againstParent;
-  private ObjectId aId;
-  private ObjectId bId;
-
-  private final Side a;
-  private final Side b;
-
-  private List<Edit> edits;
-  private final FileTypeRegistry registry;
-  private final PatchListCache patchListCache;
-  private int context;
-
-  @Inject
-  PatchScriptBuilder(final FileTypeRegistry ftr, final PatchListCache plc) {
-    a = new Side();
-    b = new Side();
-    registry = ftr;
-    patchListCache = plc;
-  }
-
-  void setRepository(Repository r, Project.NameKey projectKey) {
-    this.db = r;
-    this.projectKey = projectKey;
-  }
-
-  void setChange(final Change c) {
-    this.change = c;
-  }
-
-  void setDiffPrefs(final AccountDiffPreference dp) {
-    diffPrefs = dp;
-
-    context = diffPrefs.getContext();
-    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
-      context = MAX_CONTEXT;
-    } else if (context > MAX_CONTEXT) {
-      context = MAX_CONTEXT;
-    }
-  }
-
-  void setTrees(final boolean ap, final ObjectId a, final ObjectId b) {
-    againstParent = ap;
-    aId = a;
-    bId = b;
-  }
-
-  PatchScript toPatchScript(final PatchListEntry content,
-      final CommentDetail comments, final List<Patch> history)
-      throws IOException {
-    reader = db.newObjectReader();
-    try {
-      return build(content, comments, history);
-    } finally {
-      reader.release();
-    }
-  }
-
-  private PatchScript build(final PatchListEntry content,
-      final CommentDetail comments, final List<Patch> history)
-      throws IOException {
-    boolean intralineDifferenceIsPossible = true;
-    boolean intralineFailure = false;
-    boolean intralineTimeout = false;
-
-    a.path = oldName(content);
-    b.path = newName(content);
-
-    a.resolve(null, aId);
-    b.resolve(a, bId);
-
-    edits = new ArrayList<Edit>(content.getEdits());
-
-    if (!isModify(content)) {
-      intralineDifferenceIsPossible = false;
-    } else if (diffPrefs.isIntralineDifference()) {
-      IntraLineDiff d =
-          patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
-              b.id, b.src, edits, projectKey, bId, b.path,
-              diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE));
-      if (d != null) {
-        switch (d.getStatus()) {
-          case EDIT_LIST:
-            edits = new ArrayList<Edit>(d.getEdits());
-            break;
-
-          case DISABLED:
-            intralineDifferenceIsPossible = false;
-            break;
-
-          case ERROR:
-            intralineDifferenceIsPossible = false;
-            intralineFailure = true;
-            break;
-
-          case TIMEOUT:
-            intralineDifferenceIsPossible = false;
-            intralineTimeout = true;
-            break;
-        }
-      } else {
-        intralineDifferenceIsPossible = false;
-        intralineFailure = true;
-      }
-    }
-
-    ensureCommentsVisible(comments);
-
-    boolean hugeFile = false;
-    if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
-
-    } else if (a.src == b.src && a.size() <= context
-        && content.getEdits().isEmpty()) {
-      // Odd special case; the files are identical (100% rename or copy)
-      // and the user has asked for context that is larger than the file.
-      // Send them the entire file, with an empty edit after the last line.
-      //
-      for (int i = 0; i < a.size(); i++) {
-        a.addLine(i);
-      }
-      edits = new ArrayList<Edit>(1);
-      edits.add(new Edit(a.size(), a.size()));
-
-    } else {
-      if (BIG_FILE < Math.max(a.size(), b.size())) {
-        // IF the file is really large, we disable things to avoid choking
-        // the browser client.
-        //
-        diffPrefs.setContext((short) Math.min(25, context));
-        diffPrefs.setSyntaxHighlighting(false);
-        context = diffPrefs.getContext();
-        hugeFile = true;
-
-      } else {
-        // In order to expand the skipped common lines or syntax highlight the
-        // file properly we need to give the client the complete file contents.
-        // So force our context temporarily to the complete file size.
-        //
-        context = MAX_CONTEXT;
-      }
-      packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
-    }
-
-    return new PatchScript(change.getKey(), content.getChangeType(),
-        content.getOldName(), content.getNewName(), a.fileMode, b.fileMode,
-        content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
-        a.displayMethod, b.displayMethod, comments, history, hugeFile,
-        intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
-  }
-
-  private static boolean isModify(PatchListEntry content) {
-    switch (content.getChangeType()) {
-      case MODIFIED:
-      case COPIED:
-      case RENAMED:
-        return true;
-
-      case ADDED:
-      case DELETED:
-      default:
-        return false;
-    }
-  }
-
-  private static String oldName(final PatchListEntry entry) {
-    switch (entry.getChangeType()) {
-      case ADDED:
-        return null;
-      case DELETED:
-      case MODIFIED:
-        return entry.getNewName();
-      case COPIED:
-      case RENAMED:
-      default:
-        return entry.getOldName();
-    }
-  }
-
-  private static String newName(final PatchListEntry entry) {
-    switch (entry.getChangeType()) {
-      case DELETED:
-        return null;
-      case ADDED:
-      case MODIFIED:
-      case COPIED:
-      case RENAMED:
-      default:
-        return entry.getNewName();
-    }
-  }
-
-  private void ensureCommentsVisible(final CommentDetail comments) {
-    if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) {
-      // No comments, no additional dummy edits are required.
-      //
-      return;
-    }
-
-    // Construct empty Edit blocks around each location where a comment is.
-    // This will force the later packContent method to include the regions
-    // containing comments, potentially combining those regions together if
-    // they have overlapping contexts. UI renders will also be able to make
-    // correct hunks from this, but because the Edit is empty they will not
-    // style it specially.
-    //
-    final List<Edit> empty = new ArrayList<Edit>();
-    int lastLine;
-
-    lastLine = -1;
-    for (PatchLineComment plc : comments.getCommentsA()) {
-      final int a = plc.getLine();
-      if (lastLine != a) {
-        final int b = mapA2B(a - 1);
-        if (0 <= b) {
-          safeAdd(empty, new Edit(a - 1, b));
-        }
-        lastLine = a;
-      }
-    }
-
-    lastLine = -1;
-    for (PatchLineComment plc : comments.getCommentsB()) {
-      final int b = plc.getLine();
-      if (lastLine != b) {
-        final int a = mapB2A(b - 1);
-        if (0 <= a) {
-          safeAdd(empty, new Edit(a, b - 1));
-        }
-        lastLine = b;
-      }
-    }
-
-    // Sort the final list by the index in A, so packContent can combine
-    // them correctly later.
-    //
-    edits.addAll(empty);
-    Collections.sort(edits, EDIT_SORT);
-  }
-
-  private void safeAdd(final List<Edit> empty, final Edit toAdd) {
-    final int a = toAdd.getBeginA();
-    final int b = toAdd.getBeginB();
-    for (final Edit e : edits) {
-      if (e.getBeginA() <= a && a <= e.getEndA()) {
-        return;
-      }
-      if (e.getBeginB() <= b && b <= e.getEndB()) {
-        return;
-      }
-    }
-    empty.add(toAdd);
-  }
-
-  private int mapA2B(final int a) {
-    if (edits.isEmpty()) {
-      // Magic special case of an unmodified file.
-      //
-      return a;
-    }
-
-    for (int i = 0; i < edits.size(); i++) {
-      final Edit e = edits.get(i);
-      if (a < e.getBeginA()) {
-        if (i == 0) {
-          // Special case of context at start of file.
-          //
-          return a;
-        }
-        return e.getBeginB() - (e.getBeginA() - a);
-      }
-      if (e.getBeginA() <= a && a <= e.getEndA()) {
-        return -1;
-      }
-    }
-
-    final Edit last = edits.get(edits.size() - 1);
-    return last.getEndB() + (a - last.getEndA());
-  }
-
-  private int mapB2A(final int b) {
-    if (edits.isEmpty()) {
-      // Magic special case of an unmodified file.
-      //
-      return b;
-    }
-
-    for (int i = 0; i < edits.size(); i++) {
-      final Edit e = edits.get(i);
-      if (b < e.getBeginB()) {
-        if (i == 0) {
-          // Special case of context at start of file.
-          //
-          return b;
-        }
-        return e.getBeginA() - (e.getBeginB() - b);
-      }
-      if (e.getBeginB() <= b && b <= e.getEndB()) {
-        return -1;
-      }
-    }
-
-    final Edit last = edits.get(edits.size() - 1);
-    return last.getEndA() + (b - last.getEndB());
-  }
-
-  private void packContent(boolean ignoredWhitespace) {
-    EditList list = new EditList(edits, context, a.size(), b.size());
-    for (final EditList.Hunk hunk : list.getHunks()) {
-      while (hunk.next()) {
-        if (hunk.isContextLine()) {
-          final String lineA = a.src.getString(hunk.getCurA());
-          a.dst.addLine(hunk.getCurA(), lineA);
-
-          if (ignoredWhitespace) {
-            // If we ignored whitespace in some form, also get the line
-            // from b when it does not exactly match the line from a.
-            //
-            final String lineB = b.src.getString(hunk.getCurB());
-            if (!lineA.equals(lineB)) {
-              b.dst.addLine(hunk.getCurB(), lineB);
-            }
-          }
-          hunk.incBoth();
-          continue;
-        }
-
-        if (hunk.isDeletedA()) {
-          a.addLine(hunk.getCurA());
-          hunk.incA();
-        }
-
-        if (hunk.isInsertedB()) {
-          b.addLine(hunk.getCurB());
-          hunk.incB();
-        }
-      }
-    }
-  }
-
-  private class Side {
-    String path;
-    ObjectId id;
-    FileMode mode;
-    byte[] srcContent;
-    Text src;
-    MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
-    DisplayMethod displayMethod = DisplayMethod.DIFF;
-    PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
-    final SparseFileContent dst = new SparseFileContent();
-
-    int size() {
-      return src != null ? src.size() : 0;
-    }
-
-    void addLine(int line) {
-      dst.addLine(line, src.getString(line));
-    }
-
-    void resolve(final Side other, final ObjectId within) throws IOException {
-      try {
-        final boolean reuse;
-        if (Patch.COMMIT_MSG.equals(path)) {
-          if (againstParent && (aId == within || within.equals(aId))) {
-            id = ObjectId.zeroId();
-            src = Text.EMPTY;
-            srcContent = Text.NO_BYTES;
-            mode = FileMode.MISSING;
-            displayMethod = DisplayMethod.NONE;
-          } else {
-            id = within;
-            src = Text.forCommit(db, reader, within);
-            srcContent = src.getContent();
-            if (src == Text.EMPTY) {
-              mode = FileMode.MISSING;
-              displayMethod = DisplayMethod.NONE;
-            } else {
-              mode = FileMode.REGULAR_FILE;
-            }
-          }
-          reuse = false;
-
-        } else {
-          final TreeWalk tw = find(within);
-
-          id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
-          mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
-          reuse = other != null && other.id.equals(id) && other.mode == mode;
-
-          if (reuse) {
-            srcContent = other.srcContent;
-
-          } else if (mode.getObjectType() == Constants.OBJ_BLOB) {
-            srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB));
-
-          } else {
-            srcContent = Text.NO_BYTES;
-          }
-
-          if (reuse) {
-            mimeType = other.mimeType;
-            displayMethod = other.displayMethod;
-            src = other.src;
-
-          } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) {
-            mimeType = registry.getMimeType(path, srcContent);
-            if ("image".equals(mimeType.getMediaType())
-                && registry.isSafeInline(mimeType)) {
-              displayMethod = DisplayMethod.IMG;
-            }
-          }
-        }
-
-        if (mode == FileMode.MISSING) {
-          displayMethod = DisplayMethod.NONE;
-        }
-
-        if (!reuse) {
-          if (srcContent == Text.NO_BYTES) {
-            src = Text.EMPTY;
-          } else {
-            src = new Text(srcContent);
-          }
-        }
-
-        if (srcContent.length > 0 && srcContent[srcContent.length - 1] != '\n') {
-          dst.setMissingNewlineAtEnd(true);
-        }
-        dst.setSize(size());
-        dst.setPath(path);
-
-        if (mode == FileMode.SYMLINK) {
-          fileMode = PatchScript.FileMode.SYMLINK;
-        } else if (mode == FileMode.GITLINK) {
-          fileMode = PatchScript.FileMode.GITLINK;
-        }
-      } catch (IOException err) {
-        throw new IOException("Cannot read " + within.name() + ":" + path, err);
-      }
-    }
-
-    private TreeWalk find(final ObjectId within) throws MissingObjectException,
-        IncorrectObjectTypeException, CorruptObjectException, IOException {
-      if (path == null || within == null) {
-        return null;
-      }
-      final RevWalk rw = new RevWalk(reader);
-      final RevTree tree = rw.parseTree(within);
-      return TreeWalk.forPath(reader, path, tree);
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
deleted file mode 100644
index 797229c..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ /dev/null
@@ -1,347 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.patch;
-
-import com.google.gerrit.common.data.CommentDetail;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Change;
-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.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-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.AccountInfoCacheFactory;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LargeObjectException;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.project.ChangeControl;
-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.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nullable;
-
-
-class PatchScriptFactory extends Handler<PatchScript> {
-  interface Factory {
-    PatchScriptFactory create(Patch.Key patchKey,
-        @Assisted("patchSetA") PatchSet.Id patchSetA,
-        @Assisted("patchSetB") PatchSet.Id patchSetB,
-        AccountDiffPreference diffPrefs);
-  }
-
-  private static final Logger log =
-      LoggerFactory.getLogger(PatchScriptFactory.class);
-
-  private final GitRepositoryManager repoManager;
-  private final Provider<PatchScriptBuilder> builderFactory;
-  private final PatchListCache patchListCache;
-  private final ReviewDb db;
-  private final ChangeControl.Factory changeControlFactory;
-  private final AccountInfoCacheFactory.Factory aicFactory;
-
-  private final Patch.Key patchKey;
-  @Nullable
-  private final PatchSet.Id psa;
-  private final PatchSet.Id psb;
-  private final AccountDiffPreference diffPrefs;
-
-  private final PatchSet.Id patchSetId;
-  private final Change.Id changeId;
-
-  private Change change;
-  private PatchSet patchSet;
-  private Project.NameKey projectKey;
-  private ChangeControl control;
-  private ObjectId aId;
-  private ObjectId bId;
-  private List<Patch> history;
-  private CommentDetail comments;
-
-  @Inject
-  PatchScriptFactory(final GitRepositoryManager grm,
-      Provider<PatchScriptBuilder> builderFactory,
-      final PatchListCache patchListCache, final ReviewDb db,
-      final ChangeControl.Factory changeControlFactory,
-      final AccountInfoCacheFactory.Factory aicFactory,
-      @Assisted final Patch.Key patchKey,
-      @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
-      @Assisted("patchSetB") final PatchSet.Id patchSetB,
-      @Assisted final AccountDiffPreference diffPrefs) {
-    this.repoManager = grm;
-    this.builderFactory = builderFactory;
-    this.patchListCache = patchListCache;
-    this.db = db;
-    this.changeControlFactory = changeControlFactory;
-    this.aicFactory = aicFactory;
-
-    this.patchKey = patchKey;
-    this.psa = patchSetA;
-    this.psb = patchSetB;
-    this.diffPrefs = diffPrefs;
-
-    patchSetId = patchKey.getParentKey();
-    changeId = patchSetId.getParentKey();
-  }
-
-  @Override
-  public PatchScript call() throws OrmException, NoSuchChangeException,
-      LargeObjectException {
-    validatePatchSetId(psa);
-    validatePatchSetId(psb);
-
-    control = changeControlFactory.validateFor(changeId);
-    change = control.getChange();
-    projectKey = change.getProject();
-    patchSet = db.patchSets().get(patchSetId);
-    if (patchSet == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    aId = psa != null ? toObjectId(db, psa) : null;
-    bId = toObjectId(db, psb);
-
-    if ((psa != null && !control.isPatchVisible(db.patchSets().get(psa), db)) ||
-        (psb != null && !control.isPatchVisible(db.patchSets().get(psb), db))) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    final Repository git;
-    try {
-      git = repoManager.openRepository(projectKey);
-    } catch (RepositoryNotFoundException e) {
-      log.error("Repository " + projectKey + " not found", e);
-      throw new NoSuchChangeException(changeId, e);
-    } catch (IOException e) {
-      log.error("Cannot open repository " + projectKey, e);
-      throw new NoSuchChangeException(changeId, e);
-    }
-    try {
-      final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
-      final PatchScriptBuilder b = newBuilder(list, git);
-      final PatchListEntry content = list.get(patchKey.getFileName());
-
-      loadCommentsAndHistory(content.getChangeType(), //
-          content.getOldName(), //
-          content.getNewName());
-
-        return b.toPatchScript(content, comments, history);
-    } catch (PatchListNotAvailableException e) {
-      throw new NoSuchChangeException(changeId, e);
-    } catch (IOException e) {
-      log.error("File content unavailable", e);
-      throw new NoSuchChangeException(changeId, e);
-    } catch (org.eclipse.jgit.errors.LargeObjectException err) {
-      throw new LargeObjectException("File content is too large", err);
-    } finally {
-      git.close();
-    }
-  }
-
-  private PatchListKey keyFor(final Whitespace whitespace) {
-    return new PatchListKey(projectKey, aId, bId, whitespace);
-  }
-
-  private PatchList listFor(final PatchListKey key)
-      throws PatchListNotAvailableException {
-    return patchListCache.get(key);
-  }
-
-  private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
-    final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
-    final PatchScriptBuilder b = builderFactory.get();
-    b.setRepository(git, projectKey);
-    b.setChange(change);
-    b.setDiffPrefs(dp);
-    b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
-    return b;
-  }
-
-  private ObjectId toObjectId(final ReviewDb db, final PatchSet.Id psId)
-      throws OrmException, NoSuchChangeException {
-    if (!changeId.equals(psId.getParentKey())) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    final PatchSet ps = db.patchSets().get(psId);
-    if (ps == null || ps.getRevision() == null
-        || ps.getRevision().get() == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    try {
-      return ObjectId.fromString(ps.getRevision().get());
-    } catch (IllegalArgumentException e) {
-      log.error("Patch set " + psId + " has invalid revision");
-      throw new NoSuchChangeException(changeId, e);
-    }
-  }
-
-  private void validatePatchSetId(final PatchSet.Id psId)
-      throws NoSuchChangeException {
-    if (psId == null) { // OK, means use base;
-    } else if (changeId.equals(psId.getParentKey())) { // OK, same change;
-    } else {
-      throw new NoSuchChangeException(changeId);
-    }
-  }
-
-  private void loadCommentsAndHistory(final ChangeType changeType,
-      final String oldName, final String newName) throws OrmException {
-    history = new ArrayList<Patch>();
-    comments = new CommentDetail(psa, psb);
-
-    final Map<Patch.Key, Patch> byKey = new HashMap<Patch.Key, Patch>();
-    final AccountInfoCacheFactory aic = aicFactory.create();
-
-    // This seems like a cheap trick. It doesn't properly account for a
-    // file that gets renamed between patch set 1 and patch set 2. We
-    // will wind up packing the wrong Patch object because we didn't do
-    // proper rename detection between the patch sets.
-    //
-    for (final PatchSet ps : db.patchSets().byChange(changeId)) {
-      if (!control.isPatchVisible(ps, db)) {
-        continue;
-      }
-      String name = patchKey.get();
-      if (psa != null) {
-        switch (changeType) {
-          case COPIED:
-          case RENAMED:
-            if (ps.getId().equals(psa)) {
-              name = oldName;
-            }
-            break;
-
-          case MODIFIED:
-          case DELETED:
-          case ADDED:
-          case REWRITE:
-            break;
-        }
-      }
-
-      final Patch p = new Patch(new Patch.Key(ps.getId(), name));
-      history.add(p);
-      byKey.put(p.getKey(), p);
-    }
-
-    switch (changeType) {
-      case ADDED:
-      case MODIFIED:
-        loadPublished(byKey, aic, newName);
-        break;
-
-      case DELETED:
-        loadPublished(byKey, aic, newName);
-        break;
-
-      case COPIED:
-      case RENAMED:
-        if (psa != null) {
-          loadPublished(byKey, aic, oldName);
-        }
-        loadPublished(byKey, aic, newName);
-        break;
-
-      case REWRITE:
-        break;
-    }
-
-    final CurrentUser user = control.getCurrentUser();
-    if (user instanceof IdentifiedUser) {
-      final Account.Id me = ((IdentifiedUser) user).getAccountId();
-      switch (changeType) {
-        case ADDED:
-        case MODIFIED:
-          loadDrafts(byKey, aic, me, newName);
-          break;
-
-        case DELETED:
-          loadDrafts(byKey, aic, me, newName);
-          break;
-
-        case COPIED:
-        case RENAMED:
-          if (psa != null) {
-            loadDrafts(byKey, aic, me, oldName);
-          }
-          loadDrafts(byKey, aic, me, newName);
-          break;
-
-        case REWRITE:
-          break;
-      }
-    }
-
-    comments.setAccountInfoCache(aic.create());
-  }
-
-  private void loadPublished(final Map<Patch.Key, Patch> byKey,
-      final AccountInfoCacheFactory aic, final String file) throws OrmException {
-    for (PatchLineComment c : db.patchComments().publishedByChangeFile(changeId, file)) {
-      if (comments.include(c)) {
-        aic.want(c.getAuthor());
-      }
-
-      final Patch.Key pKey = c.getKey().getParentKey();
-      final Patch p = byKey.get(pKey);
-      if (p != null) {
-        p.setCommentCount(p.getCommentCount() + 1);
-      }
-    }
-  }
-
-  private void loadDrafts(final Map<Patch.Key, Patch> byKey,
-      final AccountInfoCacheFactory aic, final Account.Id me, final String file)
-      throws OrmException {
-    for (PatchLineComment c : db.patchComments().draftByChangeFileAuthor(changeId, file, me)) {
-      if (comments.include(c)) {
-        aic.want(me);
-      }
-
-      final Patch.Key pKey = c.getKey().getParentKey();
-      final Patch p = byKey.get(pKey);
-      if (p != null) {
-        p.setDraftCount(p.getDraftCount() + 1);
-      }
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
index 18ab5ff5..a9b908d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
@@ -25,6 +25,7 @@
 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.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -84,12 +85,19 @@
             throw new IllegalStateException("Parent comment must be on same side");
           }
         }
+        if (comment.getRange() != null
+            && comment.getLine() != comment.getRange().getEndLine()) {
+            throw new IllegalStateException(
+              "Range endLine must be on the same line as the comment");
+        }
 
         final PatchLineComment nc =
-            new PatchLineComment(new PatchLineComment.Key(patchKey, ChangeUtil
-                .messageUUID(db)), comment.getLine(), me, comment.getParentUuid());
+            new PatchLineComment(new PatchLineComment.Key(patchKey,
+                ChangeUtil.messageUUID(db)), comment.getLine(), me,
+                comment.getParentUuid(), TimeUtil.nowTs());
         nc.setSide(comment.getSide());
         nc.setMessage(comment.getMessage());
+        nc.setRange(comment.getRange());
         db.patchComments().insert(Collections.singleton(nc));
         db.commit();
         return nc;
@@ -98,7 +106,7 @@
         if (!me.equals(comment.getAuthor())) {
           throw new NoSuchChangeException(changeId);
         }
-        comment.updated();
+        comment.updated(TimeUtil.nowTs());
         db.patchComments().update(Collections.singleton(comment));
         db.commit();
         return comment;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
deleted file mode 100644
index d26501e..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ /dev/null
@@ -1,233 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.AddBranchResult;
-import com.google.gerrit.common.errors.InvalidRevisionException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-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.util.MagicBranch;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.ObjectWalk;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-class AddBranch extends Handler<AddBranchResult> {
-  private static final Logger log = LoggerFactory.getLogger(AddBranch.class);
-
-  interface Factory {
-    AddBranch create(@Assisted Project.NameKey projectName,
-        @Assisted("branchName") String branchName,
-        @Assisted("startingRevision") String startingRevision);
-  }
-
-  private final ProjectControl.Factory projectControlFactory;
-  private final ListBranches.Factory listBranchesFactory;
-  private final IdentifiedUser identifiedUser;
-  private final GitRepositoryManager repoManager;
-  private final GitReferenceUpdated referenceUpdated;
-  private final ChangeHooks hooks;
-
-  private final Project.NameKey projectName;
-  private final String branchName;
-  private final String startingRevision;
-
-  @Inject
-  AddBranch(final ProjectControl.Factory projectControlFactory,
-      final ListBranches.Factory listBranchesFactory,
-      final IdentifiedUser identifiedUser,
-      final GitRepositoryManager repoManager,
-      GitReferenceUpdated referenceUpdated,
-      final ChangeHooks hooks,
-
-      @Assisted Project.NameKey projectName,
-      @Assisted("branchName") String branchName,
-      @Assisted("startingRevision") String startingRevision) {
-    this.projectControlFactory = projectControlFactory;
-    this.listBranchesFactory = listBranchesFactory;
-    this.identifiedUser = identifiedUser;
-    this.repoManager = repoManager;
-    this.referenceUpdated = referenceUpdated;
-    this.hooks = hooks;
-
-    this.projectName = projectName;
-    this.branchName = branchName;
-    this.startingRevision = startingRevision;
-  }
-
-  @Override
-  public AddBranchResult call() throws NoSuchProjectException, IOException {
-    final ProjectControl projectControl =
-        projectControlFactory.controlFor(projectName);
-
-    String refname = branchName;
-    while (refname.startsWith("/")) {
-      refname = refname.substring(1);
-    }
-    if (!refname.startsWith(Constants.R_REFS)) {
-      refname = Constants.R_HEADS + refname;
-    }
-    if (!Repository.isValidRefName(refname)) {
-      return new AddBranchResult(new AddBranchResult.Error(
-          AddBranchResult.Error.Type.INVALID_NAME, refname));
-    }
-    if (MagicBranch.isMagicBranch(refname)) {
-      return new AddBranchResult(
-          new AddBranchResult.Error(
-              AddBranchResult.Error.Type.BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX,
-              MagicBranch.getMagicRefNamePrefix(refname)));
-    }
-
-    final Branch.NameKey name = new Branch.NameKey(projectName, refname);
-    final RefControl refControl = projectControl.controlForRef(name);
-    final Repository repo = repoManager.openRepository(projectName);
-    try {
-      final ObjectId revid = parseStartingRevision(repo);
-      final RevWalk rw = verifyConnected(repo, revid);
-      RevObject object = rw.parseAny(revid);
-
-      if (refname.startsWith(Constants.R_HEADS)) {
-        // Ensure that what we start the branch from is a commit. If we
-        // were given a tag, deference to the commit instead.
-        //
-        try {
-          object = rw.parseCommit(object);
-        } catch (IncorrectObjectTypeException notCommit) {
-          throw new IllegalStateException(startingRevision + " not a commit");
-        }
-      }
-
-      if (!refControl.canCreate(rw, object)) {
-        throw new IllegalStateException("Cannot create " + refname);
-      }
-
-      try {
-        final RefUpdate u = repo.updateRef(refname);
-        u.setExpectedOldObjectId(ObjectId.zeroId());
-        u.setNewObjectId(object.copy());
-        u.setRefLogIdent(identifiedUser.newRefLogIdent());
-        u.setRefLogMessage("created via web from " + startingRevision, false);
-        final RefUpdate.Result result = u.update(rw);
-        switch (result) {
-          case FAST_FORWARD:
-          case NEW:
-          case NO_CHANGE:
-            referenceUpdated.fire(name.getParentKey(), u);
-            hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
-            break;
-          case LOCK_FAILURE:
-            if (repo.getRef(refname) != null) {
-              return new AddBranchResult(new AddBranchResult.Error(
-                  AddBranchResult.Error.Type.BRANCH_ALREADY_EXISTS, refname));
-            }
-            String refPrefix = getRefPrefix(refname);
-            while (!Constants.R_HEADS.equals(refPrefix)) {
-              if (repo.getRef(refPrefix) != null) {
-                return new AddBranchResult(new AddBranchResult.Error(
-                    AddBranchResult.Error.Type.BRANCH_CREATION_CONFLICT, refPrefix));
-              }
-              refPrefix = getRefPrefix(refPrefix);
-            }
-          default: {
-            throw new IOException(result.name());
-          }
-        }
-      } catch (IOException err) {
-        log.error("Cannot create branch " + name, err);
-        throw err;
-      }
-    } catch (InvalidRevisionException e) {
-      return new AddBranchResult(new AddBranchResult.Error(
-          AddBranchResult.Error.Type.INVALID_REVISION));
-    } finally {
-      repo.close();
-    }
-
-    return new AddBranchResult(listBranchesFactory.create(projectName).call());
-  }
-
-  private static String getRefPrefix(final String refName) {
-    final int i = refName.lastIndexOf('/');
-    if (i > Constants.R_HEADS.length() - 1) {
-      return refName.substring(0, i);
-    }
-    return Constants.R_HEADS;
-  }
-
-  private ObjectId parseStartingRevision(final Repository repo)
-      throws InvalidRevisionException {
-    try {
-      final ObjectId revid = repo.resolve(startingRevision);
-      if (revid == null) {
-        throw new InvalidRevisionException();
-      }
-      return revid;
-    } catch (IOException err) {
-      log.error("Cannot resolve \"" + startingRevision + "\" in project \""
-          + projectName + "\"", err);
-      throw new InvalidRevisionException();
-    }
-  }
-
-  private RevWalk verifyConnected(final Repository repo, final ObjectId revid)
-      throws InvalidRevisionException {
-    try {
-      final ObjectWalk rw = new ObjectWalk(repo);
-      try {
-        rw.markStart(rw.parseCommit(revid));
-      } catch (IncorrectObjectTypeException err) {
-        throw new InvalidRevisionException();
-      }
-      for (final Ref r : repo.getAllRefs().values()) {
-        try {
-          rw.markUninteresting(rw.parseAny(r.getObjectId()));
-        } catch (MissingObjectException err) {
-          continue;
-        }
-      }
-      rw.checkConnectivity();
-      return rw;
-    } catch (IncorrectObjectTypeException err) {
-      throw new InvalidRevisionException();
-    } catch (MissingObjectException err) {
-      throw new InvalidRevisionException();
-    } catch (IOException err) {
-      log.error("Repository \"" + repo.getDirectory()
-          + "\" may be corrupt; suggest running git fsck", err);
-      throw new InvalidRevisionException();
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 72b5e3a..0dd3d16 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.reviewdb.client.Project;
@@ -32,8 +33,6 @@
 import java.io.IOException;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 class ChangeProjectAccess extends ProjectAccessHandler<ProjectAccess> {
   interface Factory {
     ChangeProjectAccess create(@Assisted Project.NameKey projectName,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
deleted file mode 100644
index 41354aa..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.PerRequestProjectControlCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmConcurrencyException;
-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.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-
-import java.io.IOException;
-
-class ChangeProjectSettings extends Handler<ProjectDetail> {
-  interface Factory {
-    ChangeProjectSettings create(@Assisted Project update);
-  }
-
-  private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final ProjectControl.Factory projectControlFactory;
-  private final GitRepositoryManager mgr;
-  private final MetaDataUpdate.User metaDataUpdateFactory;
-  private final Provider<PerRequestProjectControlCache> userCache;
-
-  private final Project update;
-
-  @Inject
-  ChangeProjectSettings(
-      final ProjectDetailFactory.Factory projectDetailFactory,
-      final ProjectControl.Factory projectControlFactory,
-      final GitRepositoryManager mgr,
-      final MetaDataUpdate.User metaDataUpdateFactory,
-      final Provider<PerRequestProjectControlCache> uc,
-      @Assisted final Project update) {
-    this.projectDetailFactory = projectDetailFactory;
-    this.projectControlFactory = projectControlFactory;
-    this.mgr = mgr;
-    this.userCache = uc;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-
-    this.update = update;
-  }
-
-  @Override
-  public ProjectDetail call() throws NoSuchProjectException, OrmException,
-      IOException {
-    final Project.NameKey projectName = update.getNameKey();
-    projectControlFactory.ownerFor(projectName);
-
-    final MetaDataUpdate md;
-    try {
-      md = metaDataUpdateFactory.create(projectName);
-    } catch (RepositoryNotFoundException notFound) {
-      throw new NoSuchProjectException(projectName);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    try {
-      // TODO We really should take advantage of the Git commit DAG and
-      // ensure the current version matches the old version the caller read.
-      //
-      ProjectConfig config = ProjectConfig.read(md);
-      config.getProject().copySettingsFrom(update);
-
-      md.setMessage("Modified project settings\n");
-      try {
-        config.commit(md);
-        mgr.setProjectDescription(projectName, update.getDescription());
-        userCache.get().evict(config.getProject());
-      } catch (IOException e) {
-        throw new OrmConcurrencyException("Cannot update " + projectName);
-      }
-    } catch (ConfigInvalidException err) {
-      throw new OrmException("Cannot read project " + projectName, err);
-    } catch (IOException err) {
-      throw new OrmException("Cannot update project " + projectName, err);
-    } finally {
-      md.close();
-    }
-
-    return projectDetailFactory.create(projectName).call();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
deleted file mode 100644
index 8f5430d..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Set;
-
-class DeleteBranches extends Handler<Set<Branch.NameKey>> {
-  private static final Logger log =
-      LoggerFactory.getLogger(DeleteBranches.class);
-
-  interface Factory {
-    DeleteBranches create(@Assisted Project.NameKey name,
-        @Assisted Set<Branch.NameKey> toRemove);
-  }
-
-  private final ProjectControl.Factory projectControlFactory;
-  private final GitRepositoryManager repoManager;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final IdentifiedUser identifiedUser;
-  private final ChangeHooks hooks;
-  private final ReviewDb db;
-
-  private final Project.NameKey projectName;
-  private final Set<Branch.NameKey> toRemove;
-
-  @Inject
-  DeleteBranches(final ProjectControl.Factory projectControlFactory,
-      final GitRepositoryManager repoManager,
-      final GitReferenceUpdated gitRefUpdated,
-      final IdentifiedUser identifiedUser,
-      final ChangeHooks hooks,
-      final ReviewDb db,
-
-      @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
-    this.projectControlFactory = projectControlFactory;
-    this.repoManager = repoManager;
-    this.gitRefUpdated = gitRefUpdated;
-    this.identifiedUser = identifiedUser;
-    this.hooks = hooks;
-    this.db = db;
-
-    this.projectName = name;
-    this.toRemove = toRemove;
-  }
-
-  @Override
-  public Set<Branch.NameKey> call() throws NoSuchProjectException,
-      RepositoryNotFoundException, OrmException, IOException {
-    final ProjectControl projectControl =
-        projectControlFactory.controlFor(projectName);
-
-    final Iterator<Branch.NameKey> branchIt = toRemove.iterator();
-    while (branchIt.hasNext()) {
-      final Branch.NameKey k = branchIt.next();
-      if (!projectName.equals(k.getParentKey())) {
-        throw new IllegalArgumentException("All keys must be from same project");
-      }
-      if (!projectControl.controlForRef(k).canDelete()) {
-        throw new IllegalStateException("Cannot delete " + k.getShortName());
-      }
-
-      if (db.changes().byBranchOpenAll(k).iterator().hasNext()) {
-        branchIt.remove();
-      }
-    }
-
-    final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
-    final Repository r = repoManager.openRepository(projectName);
-    try {
-      for (final Branch.NameKey branchKey : toRemove) {
-        final String refname = branchKey.get();
-        final RefUpdate.Result result;
-        final RefUpdate u;
-        try {
-          u = r.updateRef(refname);
-          u.setForceUpdate(true);
-          result = u.delete();
-        } catch (IOException e) {
-          log.error("Cannot delete " + branchKey, e);
-          continue;
-        }
-
-        switch (result) {
-          case NEW:
-          case NO_CHANGE:
-          case FAST_FORWARD:
-          case FORCED:
-            deleted.add(branchKey);
-            gitRefUpdated.fire(projectName, u);
-            hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
-            break;
-
-          case REJECTED_CURRENT_BRANCH:
-            log.warn("Cannot delete " + branchKey + ": " + result.name());
-            break;
-
-          default:
-            log.error("Cannot delete " + branchKey + ": " + result.name());
-            break;
-        }
-      }
-    } finally {
-      r.close();
-    }
-    return deleted;
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
deleted file mode 100644
index 2366423..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ListBranchesResult;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-class ListBranches extends Handler<ListBranchesResult> {
-  interface Factory {
-    ListBranches create(@Assisted Project.NameKey name);
-  }
-
-  private final ProjectControl.Factory projectControlFactory;
-  private final GitRepositoryManager repoManager;
-
-  private final Project.NameKey projectName;
-
-  @Inject
-  ListBranches(final ProjectControl.Factory projectControlFactory,
-      final GitRepositoryManager repoManager,
-
-      @Assisted final Project.NameKey name) {
-    this.projectControlFactory = projectControlFactory;
-    this.repoManager = repoManager;
-
-    this.projectName = name;
-  }
-
-  @Override
-  public ListBranchesResult call() throws NoSuchProjectException, IOException {
-    final ProjectControl pctl = projectControlFactory.validateFor( //
-        projectName, //
-        ProjectControl.OWNER | ProjectControl.VISIBLE);
-
-    final List<Branch> branches = new ArrayList<Branch>();
-    Branch headBranch = null;
-    Branch configBranch = null;
-    final Set<String> targets = new HashSet<String>();
-
-    final Repository db;
-    try {
-      db = repoManager.openRepository(projectName);
-    } catch (RepositoryNotFoundException noGitRepository) {
-      return new ListBranchesResult(branches, false, true);
-    }
-    try {
-      final Map<String, Ref> all = db.getAllRefs();
-
-      if (!all.containsKey(Constants.HEAD)) {
-        // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
-        // filtered it out. If we ask for it individually we can find the
-        // underlying target and put it into the map anyway.
-        //
-        try {
-          Ref head = db.getRef(Constants.HEAD);
-          if (head != null) {
-            all.put(Constants.HEAD, head);
-          }
-        } catch (IOException e) {
-          // Ignore the failure reading HEAD.
-        }
-      }
-
-      for (final Ref ref : all.values()) {
-        if (ref.isSymbolic()) {
-          targets.add(ref.getTarget().getName());
-        }
-      }
-
-      for (final Ref ref : all.values()) {
-        if (ref.isSymbolic()) {
-          // A symbolic reference to another branch, instead of
-          // showing the resolved value, show the name it references.
-          //
-          String target = ref.getTarget().getName();
-          RefControl targetRefControl = pctl.controlForRef(target);
-          if (!targetRefControl.isVisible()) {
-            continue;
-          }
-          if (target.startsWith(Constants.R_HEADS)) {
-            target = target.substring(Constants.R_HEADS.length());
-          }
-
-          Branch b = createBranch(ref.getName());
-          b.setRevision(new RevId(target));
-
-          if (Constants.HEAD.equals(ref.getName())) {
-            b.setCanDelete(false);
-            headBranch = b;
-          } else {
-            b.setCanDelete(targetRefControl.canDelete());
-            branches.add(b);
-          }
-          continue;
-        }
-
-        final RefControl refControl = pctl.controlForRef(ref.getName());
-        if (refControl.isVisible()) {
-          if (ref.getName().startsWith(Constants.R_HEADS)) {
-            branches.add(createBranch(ref, refControl, targets));
-          } else if (GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
-            configBranch = createBranch(ref, refControl, targets);
-          }
-        }
-      }
-    } finally {
-      db.close();
-    }
-    Collections.sort(branches, new Comparator<Branch>() {
-      @Override
-      public int compare(final Branch a, final Branch b) {
-        return a.getName().compareTo(b.getName());
-      }
-    });
-    if (configBranch != null) {
-      branches.add(0, configBranch);
-    }
-    if (headBranch != null) {
-      branches.add(0, headBranch);
-    }
-    return new ListBranchesResult(branches, pctl.canAddRefs(), false);
-  }
-
-  private Branch createBranch(final Ref ref, final RefControl refControl,
-      final Set<String> targets) {
-    final Branch b = createBranch(ref.getName());
-    if (ref.getObjectId() != null) {
-      b.setRevision(new RevId(ref.getObjectId().name()));
-    }
-    b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
-    return b;
-  }
-
-  private Branch createBranch(final String name) {
-    return new Branch(new Branch.NameKey(projectName, name));
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
index 0e46bc3..66ba75c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
@@ -15,12 +15,8 @@
 package com.google.gerrit.httpd.rpc.project;
 
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.AddBranchResult;
-import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -29,49 +25,19 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.util.List;
-import java.util.Set;
 
 class ProjectAdminServiceImpl implements ProjectAdminService {
-  private final AddBranch.Factory addBranchFactory;
   private final ChangeProjectAccess.Factory changeProjectAccessFactory;
   private final ReviewProjectAccess.Factory reviewProjectAccessFactory;
-  private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
-  private final DeleteBranches.Factory deleteBranchesFactory;
-  private final ListBranches.Factory listBranchesFactory;
-  private final VisibleProjectDetails.Factory visibleProjectDetailsFactory;
   private final ProjectAccessFactory.Factory projectAccessFactory;
-  private final ProjectDetailFactory.Factory projectDetailFactory;
 
   @Inject
-  ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
-      final ChangeProjectAccess.Factory changeProjectAccessFactory,
+  ProjectAdminServiceImpl(final ChangeProjectAccess.Factory changeProjectAccessFactory,
       final ReviewProjectAccess.Factory reviewProjectAccessFactory,
-      final ChangeProjectSettings.Factory changeProjectSettingsFactory,
-      final DeleteBranches.Factory deleteBranchesFactory,
-      final ListBranches.Factory listBranchesFactory,
-      final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
-      final ProjectAccessFactory.Factory projectAccessFactory,
-      final ProjectDetailFactory.Factory projectDetailFactory) {
-    this.addBranchFactory = addBranchFactory;
+      final ProjectAccessFactory.Factory projectAccessFactory) {
     this.changeProjectAccessFactory = changeProjectAccessFactory;
     this.reviewProjectAccessFactory = reviewProjectAccessFactory;
-    this.changeProjectSettingsFactory = changeProjectSettingsFactory;
-    this.deleteBranchesFactory = deleteBranchesFactory;
-    this.listBranchesFactory = listBranchesFactory;
-    this.visibleProjectDetailsFactory = visibleProjectDetailsFactory;
     this.projectAccessFactory = projectAccessFactory;
-    this.projectDetailFactory = projectDetailFactory;
-  }
-
-  @Override
-  public void visibleProjectDetails(final AsyncCallback<List<ProjectDetail>> callback) {
-    visibleProjectDetailsFactory.create().to(callback);
-  }
-
-  @Override
-  public void projectDetail(final Project.NameKey projectName,
-      final AsyncCallback<ProjectDetail> callback) {
-    projectDetailFactory.create(projectName).to(callback);
   }
 
   @Override
@@ -80,12 +46,6 @@
     projectAccessFactory.create(projectName).to(callback);
   }
 
-  @Override
-  public void changeProjectSettings(final Project update,
-      final AsyncCallback<ProjectDetail> callback) {
-    changeProjectSettingsFactory.create(update).to(callback);
-  }
-
   private static ObjectId getBase(final String baseRevision) {
     if (baseRevision != null && !baseRevision.isEmpty()) {
       return ObjectId.fromString(baseRevision);
@@ -106,25 +66,4 @@
       AsyncCallback<Change.Id> cb) {
     reviewProjectAccessFactory.create(projectName, getBase(baseRevision), sections, msg).to(cb);
   }
-
-  @Override
-  public void listBranches(final Project.NameKey projectName,
-      final AsyncCallback<ListBranchesResult> callback) {
-    listBranchesFactory.create(projectName).to(callback);
-  }
-
-  @Override
-  public void deleteBranch(final Project.NameKey projectName,
-      final Set<Branch.NameKey> toRemove,
-      final AsyncCallback<Set<Branch.NameKey>> callback) {
-    deleteBranchesFactory.create(projectName, toRemove).to(callback);
-  }
-
-  @Override
-  public void addBranch(final Project.NameKey projectName,
-      final String branchName, final String startingRevision,
-      final AsyncCallback<AddBranchResult> callback) {
-    addBranchFactory.create(projectName, branchName, startingRevision).to(
-        callback);
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
deleted file mode 100644
index 2533feb..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.common.collect.Iterables;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.InheritedBoolean;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-
-class ProjectDetailFactory extends Handler<ProjectDetail> {
-  interface Factory {
-    ProjectDetailFactory create(@Assisted Project.NameKey name);
-  }
-
-  private final ProjectControl.Factory projectControlFactory;
-  private final GitRepositoryManager gitRepositoryManager;
-
-  private final Project.NameKey projectName;
-
-  @Inject
-  ProjectDetailFactory(final ProjectControl.Factory projectControlFactory,
-      final GitRepositoryManager gitRepositoryManager,
-      @Assisted final Project.NameKey name) {
-    this.projectControlFactory = projectControlFactory;
-    this.gitRepositoryManager = gitRepositoryManager;
-    this.projectName = name;
-  }
-
-  @Override
-  public ProjectDetail call() throws NoSuchProjectException, IOException {
-    final ProjectControl pc =
-        projectControlFactory.validateFor(projectName, ProjectControl.OWNER
-            | ProjectControl.VISIBLE);
-    final ProjectState projectState = pc.getProjectState();
-    final ProjectDetail detail = new ProjectDetail();
-    detail.setProject(projectState.getProject());
-
-    final boolean userIsOwner = pc.isOwner();
-    final boolean userIsOwnerAnyRef = pc.isOwnerAnyRef();
-
-    detail.setCanModifyAccess(userIsOwnerAnyRef);
-    detail.setCanModifyAgreements(userIsOwner);
-    detail.setCanModifyDescription(userIsOwner);
-    detail.setCanModifyMergeType(userIsOwner);
-    detail.setCanModifyState(userIsOwner);
-
-    final InheritedBoolean useContributorAgreements = new InheritedBoolean();
-    final InheritedBoolean useSignedOffBy = new InheritedBoolean();
-    final InheritedBoolean useContentMerge = new InheritedBoolean();
-    final InheritedBoolean requireChangeID = new InheritedBoolean();
-    useContributorAgreements.setValue(projectState.getProject()
-        .getUseContributorAgreements());
-    useSignedOffBy.setValue(projectState.getProject().getUseSignedOffBy());
-    useContentMerge.setValue(projectState.getProject().getUseContentMerge());
-    requireChangeID.setValue(projectState.getProject().getRequireChangeID());
-    ProjectState parentState = Iterables.getFirst(projectState.parents(), null);
-    if (parentState != null) {
-      useContributorAgreements.setInheritedValue(parentState
-          .isUseContributorAgreements());
-      useSignedOffBy.setInheritedValue(parentState.isUseSignedOffBy());
-      useContentMerge.setInheritedValue(parentState.isUseContentMerge());
-      requireChangeID.setInheritedValue(parentState.isRequireChangeID());
-    }
-    detail.setUseContributorAgreements(useContributorAgreements);
-    detail.setUseSignedOffBy(useSignedOffBy);
-    detail.setUseContentMerge(useContentMerge);
-    detail.setRequireChangeID(requireChangeID);
-
-    final Project.NameKey projectName = projectState.getProject().getNameKey();
-    Repository git;
-    try {
-      git = gitRepositoryManager.openRepository(projectName);
-    } catch (RepositoryNotFoundException err) {
-      throw new NoSuchProjectException(projectName);
-    }
-    try {
-      Ref head = git.getRef(Constants.HEAD);
-      if (head != null && head.isSymbolic()
-          && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
-        detail.setPermissionOnly(true);
-      }
-    } catch (IOException err) {
-      throw new NoSuchProjectException(projectName);
-    } finally {
-      git.close();
-    }
-
-    return detail;
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index 2d4f210..bd5f940 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
@@ -28,15 +28,9 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AddBranch.Factory.class);
         factory(ChangeProjectAccess.Factory.class);
         factory(ReviewProjectAccess.Factory.class);
-        factory(ChangeProjectSettings.Factory.class);
-        factory(DeleteBranches.Factory.class);
-        factory(ListBranches.Factory.class);
-        factory(VisibleProjectDetails.Factory.class);
         factory(ProjectAccessFactory.Factory.class);
-        factory(ProjectDetailFactory.Factory.class);
       }
     });
     rpc(ProjectAdminServiceImpl.class);
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 251249f..27a02d9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -32,9 +34,12 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.mail.CreateChangeSender;
 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.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -42,15 +47,18 @@
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 public class ReviewProjectAccess extends ProjectAccessHandler<Change.Id> {
+  private static final Logger log =
+      LoggerFactory.getLogger(ReviewProjectAccess.class);
+
   interface Factory {
     ReviewProjectAccess create(@Assisted Project.NameKey projectName,
         @Nullable @Assisted ObjectId base,
@@ -63,17 +71,22 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final Provider<PostReviewers> reviewersProvider;
   private final ChangeControl.GenericFactory changeFactory;
+  private final ChangeIndexer indexer;
+  private final ChangeHooks hooks;
+  private final CreateChangeSender.Factory createChangeSenderFactory;
 
   @Inject
   ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
-      final GroupBackend groupBackend,
-      final MetaDataUpdate.User metaDataUpdateFactory, final ReviewDb db,
-      final IdentifiedUser user, final PatchSetInfoFactory patchSetInfoFactory,
-      final Provider<PostReviewers> reviewersProvider,
-      final ChangeControl.GenericFactory changeFactory,
+      GroupBackend groupBackend,
+      MetaDataUpdate.User metaDataUpdateFactory, ReviewDb db,
+      IdentifiedUser user, PatchSetInfoFactory patchSetInfoFactory,
+      Provider<PostReviewers> reviewersProvider,
+      ChangeControl.GenericFactory changeFactory,
+      ChangeIndexer indexer, ChangeHooks hooks,
+      CreateChangeSender.Factory createChangeSenderFactory,
 
-      @Assisted final Project.NameKey projectName,
-      @Nullable @Assisted final ObjectId base,
+      @Assisted Project.NameKey projectName,
+      @Nullable @Assisted ObjectId base,
       @Assisted List<AccessSection> sectionList,
       @Nullable @Assisted String message) {
     super(projectControlFactory, groupBackend, metaDataUpdateFactory,
@@ -83,6 +96,9 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.reviewersProvider = reviewersProvider;
     this.changeFactory = changeFactory;
+    this.indexer = indexer;
+    this.hooks = hooks;
+    this.createChangeSenderFactory = createChangeSenderFactory;
   }
 
   @Override
@@ -102,7 +118,8 @@
         user.getAccountId(),
         new Branch.NameKey(
             config.getProject().getNameKey(),
-            GitRepositoryManager.REF_CONFIG));
+            GitRepositoryManager.REF_CONFIG),
+        TimeUtil.nowTs());
 
     ps.setCreatedOn(change.getCreatedOn());
     ps.setUploader(change.getOwner());
@@ -117,11 +134,22 @@
       insertAncestors(ps.getId(), commit);
       db.patchSets().insert(Collections.singleton(ps));
       db.changes().insert(Collections.singleton(change));
-      addProjectOwnersAsReviewers(change);
       db.commit();
     } finally {
       db.rollback();
     }
+    indexer.index(change);
+    hooks.doPatchsetCreatedHook(change, ps, db);
+    try {
+      CreateChangeSender cm =
+          createChangeSenderFactory.create(change);
+      cm.setFrom(change.getOwner());
+      cm.setPatchSet(ps, info);
+      cm.send();
+    } catch (Exception err) {
+      log.error("Cannot send email for new change " + change.getId(), err);
+    }
+    addProjectOwnersAsReviewers(change);
     return changeId;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
deleted file mode 100644
index 1c22d83..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-class VisibleProjectDetails extends Handler<List<ProjectDetail>> {
-
-  interface Factory {
-    VisibleProjectDetails create();
-  }
-
-  private final ProjectCache projectCache;
-  private final ProjectDetailFactory.Factory projectDetailFactory;
-
-  @Inject
-  VisibleProjectDetails(final ProjectCache projectCache,
-      final ProjectDetailFactory.Factory projectDetailFactory) {
-    this.projectCache = projectCache;
-    this.projectDetailFactory = projectDetailFactory;
-  }
-
-  @Override
-  public List<ProjectDetail> call() {
-    List<ProjectDetail> result = new ArrayList<ProjectDetail>();
-    for (Project.NameKey projectName : projectCache.all()) {
-      try {
-        result.add(projectDetailFactory.create(projectName).call());
-      } catch (NoSuchProjectException e) {
-      } catch (IOException e) {
-      }
-    }
-    Collections.sort(result, new Comparator<ProjectDetail>() {
-      public int compare(final ProjectDetail a, final ProjectDetail b) {
-        return a.project.getName().compareTo(b.project.getName());
-      }
-    });
-    return result;
-  }
-}
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
index ce100a5..9f3fa1e 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
@@ -44,9 +44,9 @@
       </noscript>
     </div>
     <div id="gerrit_body"></div>
-    <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+    <div style="clear: both">
       <div id="gerrit_footer"></div>
-      <div id="gerrit_btmmenu" style="clear: both;"></div>
+      <div id="gerrit_btmmenu"></div>
     </div>
     <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
     <script id="gerrit_module"></script>
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
deleted file mode 100644
index 5cfde6a..0000000
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
+++ /dev/null
@@ -1,374 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
-import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.google.gerrit.common.data.ListBranchesResult;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
-
-import org.easymock.IExpectationSetters;
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class ListBranchesTest extends LocalDiskRepositoryTestCase {
-  static {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
-  private ObjectId idA;
-  private Project.NameKey name;
-  private Repository realDb;
-  private Repository mockDb;
-  private ProjectControl.Factory pcf;
-  private ProjectControl pc;
-  private GitRepositoryManager grm;
-  private List<RefControl> refMocks;
-
-  @Override
-  @Before
-  public void setUp() throws Exception {
-    super.setUp();
-
-    idA = ObjectId.fromString("df84c2f4f7ce7e0b25cdeac84b8870bcff319885");
-    name = new Project.NameKey("test");
-    realDb = createBareRepository();
-
-    mockDb = createStrictMock(Repository.class);
-    pc = createStrictMock(ProjectControl.class);
-    pcf = createStrictMock(ProjectControl.Factory.class);
-    grm = createStrictMock(GitRepositoryManager.class);
-    refMocks = new ArrayList<RefControl>();
-  }
-
-  private IExpectationSetters<ProjectControl> validate()
-      throws NoSuchProjectException {
-    return expect(pcf.validateFor(eq(name), //
-        eq(ProjectControl.OWNER | ProjectControl.VISIBLE)));
-  }
-
-  private void doReplay() {
-    replay(mockDb, pc, pcf, grm);
-    replay(refMocks.toArray());
-  }
-
-  private void doVerify() {
-    verify(mockDb, pc, pcf, grm);
-    verify(refMocks.toArray());
-  }
-
-  private void set(String branch, ObjectId id) throws IOException {
-    final RefUpdate u = realDb.updateRef(R_HEADS + branch);
-    u.setForceUpdate(true);
-    u.setNewObjectId(id);
-    switch (u.update()) {
-      case NEW:
-      case FAST_FORWARD:
-      case FORCED:
-        break;
-      default:
-        fail("unexpected update failure " + branch + " " + u.getResult());
-    }
-  }
-
-  @Test
-  public void testProjectNotVisible() throws Exception {
-    final NoSuchProjectException err = new NoSuchProjectException(name);
-    validate().andThrow(err);
-    doReplay();
-    try {
-      new ListBranches(pcf, grm, name).call();
-      fail("did not throw when expected not authorized");
-    } catch (NoSuchProjectException e2) {
-      assertSame(err, e2);
-    }
-    doVerify();
-  }
-
-
-  private ListBranchesResult permitted(boolean getHead)
-      throws NoSuchProjectException, IOException {
-    Map<String, Ref> refs = realDb.getAllRefs();
-
-    validate().andReturn(pc);
-
-    expect(grm.openRepository(eq(name))).andReturn(mockDb);
-    expect(mockDb.getAllRefs()).andDelegateTo(realDb);
-    if (getHead) {
-      expect(mockDb.getRef(HEAD)).andDelegateTo(realDb);
-      if (!refs.containsKey(HEAD) && realDb.getRef(HEAD) != null) {
-        refs.put(HEAD, realDb.getRef(HEAD));
-      }
-    }
-
-    Set<String> targets = targets(refs);
-    for (Ref ref : refs.values()) {
-      assumeVisible(ref, true, targets);
-    }
-
-    mockDb.close();
-
-    expect(pc.canAddRefs()).andReturn(true);
-
-    expectLastCall();
-
-    doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
-    doVerify();
-    assertNotNull(r);
-    assertNotNull(r.getBranches());
-    return r;
-  }
-
-  private Set<String> targets(Map<String, Ref> refs) {
-    Set<String> targets = new HashSet<String>();
-    for (Ref ref : refs.values()) {
-      if (ref.isSymbolic()) {
-        targets.add(ref.getLeaf().getName());
-      }
-    }
-    return targets;
-  }
-
-  private void assumeVisible(Ref ref, boolean visible, Set<String> targets) {
-    RefControl rc = createStrictMock(RefControl.class);
-    refMocks.add(rc);
-    expect(rc.isVisible()).andReturn(visible);
-    if (visible && !ref.isSymbolic() && !targets.contains(ref.getName())) {
-      expect(rc.canDelete()).andReturn(true);
-    }
-
-    if (ref.isSymbolic()) {
-      expect(pc.controlForRef(ref.getTarget().getName())).andReturn(rc);
-    } else {
-      expect(pc.controlForRef(ref.getName())).andReturn(rc);
-    }
-  }
-
-  @Test
-  public void testEmptyProject() throws Exception {
-    ListBranchesResult r = permitted(true);
-
-    assertEquals(1, r.getBranches().size());
-
-    Branch b = r.getBranches().get(0);
-    assertNotNull(b);
-
-    assertNotNull(b.getNameKey());
-    assertSame(name, b.getNameKey().getParentKey());
-    assertEquals(HEAD, b.getNameKey().get());
-
-    assertEquals(HEAD, b.getName());
-    assertEquals(HEAD, b.getShortName());
-
-    assertNotNull(b.getRevision());
-    assertEquals("master", b.getRevision().get());
-  }
-
-  @Test
-  public void testMasterBranch() throws Exception {
-    set("master", idA);
-
-    ListBranchesResult r = permitted(false);
-    assertEquals(2, r.getBranches().size());
-
-    Branch b = r.getBranches().get(0);
-    assertNotNull(b);
-
-    assertNotNull(b.getNameKey());
-    assertSame(name, b.getNameKey().getParentKey());
-    assertEquals(HEAD, b.getNameKey().get());
-
-    assertEquals(HEAD, b.getName());
-    assertEquals(HEAD, b.getShortName());
-
-    assertNotNull(b.getRevision());
-    assertEquals("master", b.getRevision().get());
-
-    b = r.getBranches().get(1);
-    assertNotNull(b);
-
-    assertNotNull(b.getNameKey());
-    assertSame(name, b.getNameKey().getParentKey());
-    assertEquals(R_HEADS + "master", b.getNameKey().get());
-
-    assertEquals(R_HEADS + "master", b.getName());
-    assertEquals("master", b.getShortName());
-
-    assertNotNull(b.getRevision());
-    assertEquals(idA.name(), b.getRevision().get());
-  }
-
-  @Test
-  public void testBranchNotHead() throws Exception {
-    set("foo", idA);
-
-    ListBranchesResult r = permitted(true);
-    assertEquals(2, r.getBranches().size());
-
-    Branch b = r.getBranches().get(0);
-    assertNotNull(b);
-
-    assertNotNull(b.getNameKey());
-    assertSame(name, b.getNameKey().getParentKey());
-    assertEquals(HEAD, b.getNameKey().get());
-
-    assertEquals(HEAD, b.getName());
-    assertEquals(HEAD, b.getShortName());
-
-    assertNotNull(b.getRevision());
-    assertEquals("master", b.getRevision().get());
-    assertFalse(b.getCanDelete());
-
-    b = r.getBranches().get(1);
-    assertNotNull(b);
-
-    assertNotNull(b.getNameKey());
-    assertSame(name, b.getNameKey().getParentKey());
-    assertEquals(R_HEADS + "foo", b.getNameKey().get());
-
-    assertEquals(R_HEADS + "foo", b.getName());
-    assertEquals("foo", b.getShortName());
-
-    assertNotNull(b.getRevision());
-    assertEquals(idA.name(), b.getRevision().get());
-    assertTrue(b.getCanDelete());
-  }
-
-  @Test
-  public void testSortByName() throws Exception {
-    Map<String, Ref> u = new LinkedHashMap<String, Ref>();
-    u.put("foo", new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "foo", idA));
-    u.put("bar", new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "bar", idA));
-    u.put(HEAD, new SymbolicRef(HEAD, new ObjectIdRef.Unpeeled(LOOSE, R_HEADS
-        + "master", null)));
-
-    validate().andReturn(pc);
-    expect(grm.openRepository(eq(name))).andReturn(mockDb);
-    expect(mockDb.getAllRefs()).andReturn(u);
-    for (Ref ref : u.values()) {
-      assumeVisible(ref, true, targets(u));
-    }
-    expect(pc.canAddRefs()).andReturn(true);
-    mockDb.close();
-    expectLastCall();
-
-    doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
-    doVerify();
-    assertNotNull(r);
-
-    assertEquals(3, r.getBranches().size());
-    assertEquals(HEAD, r.getBranches().get(0).getShortName());
-    assertEquals("bar", r.getBranches().get(1).getShortName());
-    assertEquals("foo", r.getBranches().get(2).getShortName());
-  }
-
-  @Test
-  public void testHeadNotVisible() throws Exception {
-    ObjectIdRef.Unpeeled bar =
-        new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "bar", idA);
-    Map<String, Ref> u = new LinkedHashMap<String, Ref>();
-    u.put(bar.getName(), bar);
-    u.put(HEAD, new SymbolicRef(HEAD, bar));
-
-    validate().andReturn(pc);
-    expect(grm.openRepository(eq(name))).andReturn(mockDb);
-    expect(mockDb.getAllRefs()).andReturn(u);
-    assumeVisible(bar, false, targets(u));
-    assumeVisible(bar, false, targets(u));
-    expect(pc.canAddRefs()).andReturn(true);
-    mockDb.close();
-    expectLastCall();
-
-    doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
-    doVerify();
-    assertNotNull(r);
-    assertTrue(r.getBranches().isEmpty());
-  }
-
-  @Test
-  public void testHeadVisibleButBranchHidden() throws Exception {
-    ObjectIdRef.Unpeeled bar =
-        new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "bar", idA);
-    ObjectIdRef.Unpeeled foo =
-        new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "foo", idA);
-
-    Map<String, Ref> u = new LinkedHashMap<String, Ref>();
-    u.put(bar.getName(), bar);
-    u.put(HEAD, new SymbolicRef(HEAD, bar));
-    u.put(foo.getName(), foo);
-
-    validate().andReturn(pc);
-    expect(grm.openRepository(eq(name))).andReturn(mockDb);
-    expect(mockDb.getAllRefs()).andReturn(u);
-    assumeVisible(bar, true, targets(u));
-    assumeVisible(bar, true, targets(u));
-    assumeVisible(foo, false, targets(u));
-    expect(pc.canAddRefs()).andReturn(true);
-    mockDb.close();
-    expectLastCall();
-
-    doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
-    doVerify();
-    assertNotNull(r);
-
-    assertEquals(2, r.getBranches().size());
-
-    assertEquals(HEAD, r.getBranches().get(0).getShortName());
-    assertFalse(r.getBranches().get(0).getCanDelete());
-
-    assertEquals("bar", r.getBranches().get(1).getShortName());
-    assertFalse(r.getBranches().get(1).getCanDelete());
-  }
-}
diff --git a/gerrit-launcher/BUCK b/gerrit-launcher/BUCK
new file mode 100644
index 0000000..344e53d
--- /dev/null
+++ b/gerrit-launcher/BUCK
@@ -0,0 +1,10 @@
+java_library(
+  name = 'launcher',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = ['//lib/joda:joda-time'],
+  visibility = [
+    '//gerrit-acceptance-tests/...',
+    '//gerrit-main:main_lib',
+    '//gerrit-pgm:',
+  ],
+)
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
deleted file mode 100644
index e04acca..0000000
--- a/gerrit-launcher/pom.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-launcher</artifactId>
-  <name>Gerrit Code Review - Launcher</name>
-
-  <description>
-    Bootstraps the rest of our classpath after Main
-  </description>
-</project>
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 d49c6c7..2dd20a2 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
@@ -17,6 +17,8 @@
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
+import org.joda.time.DateTimeUtils;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -299,7 +301,7 @@
     return name;
   }
 
-  private static File myArchive;
+  private volatile static File myArchive;
 
   /**
    * Locate the JAR/WAR file we were launched from.
@@ -506,7 +508,7 @@
     //
     final File[] tmpEntries = tmp.listFiles();
     if (tmpEntries != null) {
-      final long now = System.currentTimeMillis();
+      final long now = DateTimeUtils.currentTimeMillis();
       final long expired = now - MILLISECONDS.convert(7, DAYS);
       for (final File tmpEntry : tmpEntries) {
         if (tmpEntry.isDirectory() && tmpEntry.lastModified() < expired) {
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
new file mode 100644
index 0000000..5a907eb
--- /dev/null
+++ b/gerrit-lucene/BUCK
@@ -0,0 +1,39 @@
+QUERY_BUILDER = [
+  'src/main/java/com/google/gerrit/lucene/QueryBuilder.java',
+]
+
+java_library(
+  name = 'query_builder',
+  srcs = QUERY_BUILDER,
+  deps = [
+    '//gerrit-antlr:query_exception',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:gwtorm',
+    '//lib:guava',
+    '//lib/lucene:core',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'lucene',
+  srcs = glob(['src/main/java/**/*.java'], excludes = QUERY_BUILDER),
+  deps = [
+    ':query_builder',
+    '//gerrit-antlr:query_exception',
+    '//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/jgit:jgit',
+    '//lib/log:api',
+    '//lib/lucene:analyzers-common',
+    '//lib/lucene:core',
+  ],
+  visibility = ['PUBLIC'],
+)
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
new file mode 100644
index 0000000..2748963
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -0,0 +1,472 @@
+// 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.lucene;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeField.ChangeProtoField;
+import com.google.gerrit.server.index.ChangeField.PatchSetApprovalProtoField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.Schema.Values;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.analysis.util.CharArraySet;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.document.IntField;
+import org.apache.lucene.document.LongField;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.SearcherManager;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.RAMDirectory;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Version;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Secondary index implementation using Apache Lucene.
+ * <p>
+ * Writes are managed using a single {@link IndexWriter} per process, committed
+ * aggressively. Reads use {@link SearcherManager} and periodically refresh,
+ * though there may be some lag between a committed write and it showing up to
+ * other threads' searchers.
+ */
+public class LuceneChangeIndex implements ChangeIndex {
+  private static final Logger log =
+      LoggerFactory.getLogger(LuceneChangeIndex.class);
+
+  public static final String CHANGES_OPEN = "open";
+  public static final String CHANGES_CLOSED = "closed";
+  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+  private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
+  private static final String APPROVAL_FIELD = ChangeField.APPROVAL.getName();
+
+  private static final Map<Schema<ChangeData>, Version> LUCENE_VERSIONS;
+  static {
+    ImmutableMap.Builder<Schema<ChangeData>, Version> versions =
+        ImmutableMap.builder();
+    @SuppressWarnings("deprecation")
+    Version lucene43 = Version.LUCENE_43;
+    for (Map.Entry<Integer, Schema<ChangeData>> e
+        : ChangeSchemas.ALL.entrySet()) {
+      if (e.getKey() <= 3) {
+        versions.put(e.getValue(), lucene43);
+      } else {
+        versions.put(e.getValue(), Version.LUCENE_44);
+      }
+    }
+    LUCENE_VERSIONS = versions.build();
+  }
+
+  static interface Factory {
+    LuceneChangeIndex create(Schema<ChangeData> schema, String base);
+  }
+
+  private static IndexWriterConfig getIndexWriterConfig(Version version,
+      Config cfg, String name) {
+    IndexWriterConfig writerConfig = new IndexWriterConfig(version,
+        new StandardAnalyzer(version, CharArraySet.EMPTY_SET));
+    writerConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
+    double m = 1 << 20;
+    writerConfig.setRAMBufferSizeMB(cfg.getLong("index", name, "ramBufferSize",
+          (long) (IndexWriterConfig.DEFAULT_RAM_BUFFER_SIZE_MB * m)) / m);
+    writerConfig.setMaxBufferedDocs(cfg.getInt("index", name, "maxBufferedDocs",
+          IndexWriterConfig.DEFAULT_MAX_BUFFERED_DOCS));
+    return writerConfig;
+  }
+
+  private final SitePaths sitePaths;
+  private final FillArgs fillArgs;
+  private final ListeningExecutorService executor;
+  private final File dir;
+  private final Schema<ChangeData> schema;
+  private final SubIndex openIndex;
+  private final SubIndex closedIndex;
+
+  @AssistedInject
+  LuceneChangeIndex(
+      @GerritServerConfig Config cfg,
+      SitePaths sitePaths,
+      @IndexExecutor ListeningExecutorService executor,
+      FillArgs fillArgs,
+      @Assisted Schema<ChangeData> schema,
+      @Assisted @Nullable String base) throws IOException {
+    this.sitePaths = sitePaths;
+    this.fillArgs = fillArgs;
+    this.executor = executor;
+    this.schema = schema;
+
+    if (base == null) {
+      dir = LuceneVersionManager.getDir(sitePaths, schema);
+    } else {
+      dir = new File(base);
+    }
+    Version luceneVersion = checkNotNull(
+        LUCENE_VERSIONS.get(schema),
+        "unknown Lucene version for index schema: %s", schema);
+
+    IndexWriterConfig openConfig =
+        getIndexWriterConfig(luceneVersion, cfg, "changes_open");
+    IndexWriterConfig closedConfig =
+        getIndexWriterConfig(luceneVersion, cfg, "changes_closed");
+    if (cfg.getBoolean("index", "lucene", "testInmemory", false)) {
+      openIndex = new SubIndex(new RAMDirectory(), "ramOpen", openConfig);
+      closedIndex = new SubIndex(new RAMDirectory(), "ramClosed", closedConfig);
+    } else {
+      openIndex = new SubIndex(new File(dir, CHANGES_OPEN), openConfig);
+      closedIndex = new SubIndex(new File(dir, CHANGES_CLOSED), closedConfig);
+    }
+  }
+
+  @Override
+  public void close() {
+    List<ListenableFuture<?>> closeFutures = Lists.newArrayListWithCapacity(2);
+    closeFutures.add(executor.submit(new Runnable() {
+      @Override
+      public void run() {
+        openIndex.close();
+      }
+    }));
+    closeFutures.add(executor.submit(new Runnable() {
+      @Override
+      public void run() {
+        closedIndex.close();
+      }
+    }));
+    Futures.getUnchecked(Futures.allAsList(closeFutures));
+  }
+
+  @Override
+  public Schema<ChangeData> getSchema() {
+    return schema;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void insert(ChangeData cd) throws IOException {
+    Term id = QueryBuilder.idTerm(cd);
+    Document doc = toDocument(cd);
+    try {
+      if (cd.getChange().getStatus().isOpen()) {
+        Futures.allAsList(
+            closedIndex.delete(id),
+            openIndex.insert(doc)).get();
+      } else {
+        Futures.allAsList(
+            openIndex.delete(id),
+            closedIndex.insert(doc)).get();
+      }
+    } catch (ExecutionException e) {
+      throw new IOException(e);
+    } catch (InterruptedException e) {
+      throw new IOException(e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void replace(ChangeData cd) throws IOException {
+    Term id = QueryBuilder.idTerm(cd);
+    Document doc = toDocument(cd);
+    try {
+      if (cd.getChange().getStatus().isOpen()) {
+        Futures.allAsList(
+            closedIndex.delete(id),
+            openIndex.replace(id, doc)).get();
+      } else {
+        Futures.allAsList(
+            openIndex.delete(id),
+            closedIndex.replace(id, doc)).get();
+      }
+    } catch (ExecutionException e) {
+      throw new IOException(e);
+    } catch (InterruptedException e) {
+      throw new IOException(e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void delete(ChangeData cd) throws IOException {
+    Term id = QueryBuilder.idTerm(cd);
+    try {
+      Futures.allAsList(
+          openIndex.delete(id),
+          closedIndex.delete(id)).get();
+    } catch (ExecutionException e) {
+      throw new IOException(e);
+    } catch (InterruptedException e) {
+      throw new IOException(e);
+    }
+  }
+
+  @Override
+  public void deleteAll() throws IOException {
+    openIndex.deleteAll();
+    closedIndex.deleteAll();
+  }
+
+  @Override
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+      throws QueryParseException {
+    Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
+    List<SubIndex> indexes = Lists.newArrayListWithCapacity(2);
+    if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+      indexes.add(openIndex);
+    }
+    if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+      indexes.add(closedIndex);
+    }
+    return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
+        ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
+  }
+
+  @Override
+  public void markReady(boolean ready) throws IOException {
+    try {
+      FileBasedConfig cfg = LuceneVersionManager.loadGerritIndexConfig(sitePaths);
+      LuceneVersionManager.setReady(cfg, schema.getVersion(), ready);
+      cfg.save();
+    } catch (ConfigInvalidException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private static class QuerySource implements ChangeDataSource {
+    private static final ImmutableSet<String> FIELDS =
+        ImmutableSet.of(ID_FIELD, CHANGE_FIELD, APPROVAL_FIELD);
+
+    private final List<SubIndex> indexes;
+    private final Query query;
+    private final int limit;
+    private final boolean reverse;
+
+    private QuerySource(List<SubIndex> indexes, Query query, int limit,
+        boolean reverse) {
+      this.indexes = indexes;
+      this.query = query;
+      this.limit = limit;
+      this.reverse = reverse;
+    }
+
+    @Override
+    public int getCardinality() {
+      return 10; // TODO(dborowitz): estimate from Lucene?
+    }
+
+    @Override
+    public boolean hasChange() {
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return query.toString();
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
+      Sort sort = new Sort(
+          new SortField(
+              ChangeField.SORTKEY.getName(),
+              SortField.Type.LONG,
+              // Standard order is descending by sort key, unless reversed due
+              // to a sortkey_before predicate.
+              !reverse));
+      try {
+        TopDocs[] hits = new TopDocs[indexes.size()];
+        for (int i = 0; i < indexes.size(); i++) {
+          searchers[i] = indexes.get(i).acquire();
+          hits[i] = searchers[i].search(query, limit, sort);
+        }
+        TopDocs docs = TopDocs.merge(sort, limit, hits);
+
+        List<ChangeData> result =
+            Lists.newArrayListWithCapacity(docs.scoreDocs.length);
+        for (ScoreDoc sd : docs.scoreDocs) {
+          Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
+          result.add(toChangeData(doc));
+        }
+
+        final List<ChangeData> r = Collections.unmodifiableList(result);
+        return new ResultSet<ChangeData>() {
+          @Override
+          public Iterator<ChangeData> iterator() {
+            return r.iterator();
+          }
+
+          @Override
+          public List<ChangeData> toList() {
+            return r;
+          }
+
+          @Override
+          public void close() {
+            // Do nothing.
+          }
+        };
+      } catch (IOException e) {
+        throw new OrmException(e);
+      } finally {
+        for (int i = 0; i < indexes.size(); i++) {
+          if (searchers[i] != null) {
+            try {
+              indexes.get(i).release(searchers[i]);
+            } catch (IOException e) {
+              log.warn("cannot release Lucene searcher", e);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private static ChangeData toChangeData(Document doc) {
+    BytesRef cb = doc.getBinaryValue(CHANGE_FIELD);
+    if (cb == null) {
+      int id = doc.getField(ID_FIELD).numericValue().intValue();
+      return new ChangeData(new Change.Id(id));
+    }
+
+    Change change = ChangeProtoField.CODEC.decode(
+        cb.bytes, cb.offset, cb.length);
+    ChangeData cd = new ChangeData(change);
+
+    BytesRef[] approvalsBytes = doc.getBinaryValues(APPROVAL_FIELD);
+    if (approvalsBytes != null) {
+      List<PatchSetApproval> approvals =
+          Lists.newArrayListWithCapacity(approvalsBytes.length);
+      for (BytesRef ab : approvalsBytes) {
+        approvals.add(PatchSetApprovalProtoField.CODEC.decode(
+            ab.bytes, ab.offset, ab.length));
+      }
+      cd.setCurrentApprovals(approvals);
+    }
+    return cd;
+  }
+
+  private Document toDocument(ChangeData cd) throws IOException {
+    try {
+      Document result = new Document();
+      for (Values<ChangeData> vs : schema.buildFields(cd, fillArgs)) {
+        if (vs.getValues() != null) {
+          add(result, vs);
+        }
+      }
+      return result;
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private void add(Document doc, Values<ChangeData> values)
+      throws OrmException {
+    String name = values.getField().getName();
+    FieldType<?> type = values.getField().getType();
+    Store store = store(values.getField());
+
+    if (type == FieldType.INTEGER) {
+      for (Object value : values.getValues()) {
+        doc.add(new IntField(name, (Integer) value, store));
+      }
+    } else if (type == FieldType.LONG) {
+      for (Object value : values.getValues()) {
+        doc.add(new LongField(name, (Long) value, store));
+      }
+    } else if (type == FieldType.TIMESTAMP) {
+      for (Object value : values.getValues()) {
+        int t = QueryBuilder.toIndexTime((Timestamp) value);
+        doc.add(new IntField(name, t, store));
+      }
+    } else if (type == FieldType.EXACT
+        || type == FieldType.PREFIX) {
+      for (Object value : values.getValues()) {
+        doc.add(new StringField(name, (String) value, store));
+      }
+    } else if (type == FieldType.FULL_TEXT) {
+      for (Object value : values.getValues()) {
+        doc.add(new TextField(name, (String) value, store));
+      }
+    } else if (type == FieldType.STORED_ONLY) {
+      for (Object value : values.getValues()) {
+        doc.add(new StoredField(name, (byte[]) value));
+      }
+    } else {
+      throw QueryBuilder.badFieldType(type);
+    }
+  }
+
+  private static Field.Store store(FieldDef<?, ?> f) {
+    return f.isStored() ? Field.Store.YES : Field.Store.NO;
+  }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
new file mode 100644
index 0000000..ef96374
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -0,0 +1,115 @@
+// 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.lucene;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+public class LuceneIndexModule extends LifecycleModule {
+  private final Integer singleVersion;
+  private final int threads;
+  private final String base;
+
+  public LuceneIndexModule() {
+    this(null, 0, null);
+  }
+
+  public LuceneIndexModule(Integer singleVersion, int threads,
+      String base) {
+    this.singleVersion = singleVersion;
+    this.threads = threads;
+    this.base = base;
+  }
+
+  @Override
+  protected void configure() {
+    install(new FactoryModule() {
+      @Override
+      public void configure() {
+        factory(LuceneChangeIndex.Factory.class);
+      }
+    });
+    install(new IndexModule(threads));
+    if (singleVersion == null && base == null) {
+      install(new MultiVersionModule());
+    } else {
+      install(new SingleVersionModule());
+    }
+  }
+
+  private class MultiVersionModule extends LifecycleModule {
+    @Override
+    public void configure() {
+      install(new FactoryModule() {
+        @Override
+        public void configure() {
+          factory(OnlineReindexer.Factory.class);
+        }
+      });
+      listener().to(LuceneVersionManager.class);
+    }
+  }
+
+  private class SingleVersionModule extends LifecycleModule {
+    @Override
+    public void configure() {
+      listener().to(SingleVersionListener.class);
+    }
+
+    @Provides
+    @Singleton
+    LuceneChangeIndex getIndex(LuceneChangeIndex.Factory factory,
+        SitePaths sitePaths) {
+      Schema<ChangeData> schema = singleVersion != null
+          ? ChangeSchemas.get(singleVersion)
+          : ChangeSchemas.getLatest();
+      return factory.create(schema, base);
+    }
+  }
+
+  @Singleton
+  static class SingleVersionListener implements LifecycleListener {
+    private final IndexCollection indexes;
+    private final LuceneChangeIndex index;
+
+    @Inject
+    SingleVersionListener(IndexCollection indexes,
+        LuceneChangeIndex index) {
+      this.indexes = indexes;
+      this.index = index;
+    }
+
+    @Override
+    public void start() {
+      indexes.setSearchIndex(index);
+      indexes.addWriteIndex(index);
+    }
+
+    @Override
+    public void stop() {
+      // Do nothing; indexes are closed by IndexCollection.
+    }
+  }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
new file mode 100644
index 0000000..c3570a1
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -0,0 +1,225 @@
+// 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.lucene;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.TreeMap;
+
+@Singleton
+class LuceneVersionManager implements LifecycleListener {
+  private static final Logger log = LoggerFactory
+      .getLogger(LuceneVersionManager.class);
+
+  private static final String CHANGES_PREFIX = "changes_";
+
+  private static class Version {
+    private final Schema<ChangeData> schema;
+    private final int version;
+    private final boolean exists;
+    private final boolean ready;
+
+    private Version(Schema<ChangeData> schema, int version, boolean exists,
+        boolean ready) {
+      checkArgument(schema == null || schema.getVersion() == version);
+      this.schema = schema;
+      this.version = version;
+      this.exists = exists;
+      this.ready = ready;
+    }
+  }
+
+  static File getDir(SitePaths sitePaths, Schema<ChangeData> schema) {
+    return new File(sitePaths.index_dir, String.format("%s%04d",
+        CHANGES_PREFIX, schema.getVersion()));
+  }
+
+  static FileBasedConfig loadGerritIndexConfig(SitePaths sitePaths)
+      throws ConfigInvalidException, IOException {
+    FileBasedConfig cfg = new FileBasedConfig(
+        new File(sitePaths.index_dir, "gerrit_index.config"), FS.detect());
+    cfg.load();
+    return cfg;
+  }
+
+  static void setReady(Config cfg, int version, boolean ready) {
+    cfg.setBoolean("index", Integer.toString(version), "ready", ready);
+  }
+
+  private static boolean getReady(Config cfg, int version) {
+    return cfg.getBoolean("index", Integer.toString(version), "ready", false);
+  }
+
+  private final SitePaths sitePaths;
+  private final LuceneChangeIndex.Factory indexFactory;
+  private final IndexCollection indexes;
+  private final OnlineReindexer.Factory reindexerFactory;
+
+  @Inject
+  LuceneVersionManager(
+      SitePaths sitePaths,
+      LuceneChangeIndex.Factory indexFactory,
+      IndexCollection indexes,
+      OnlineReindexer.Factory reindexerFactory) {
+    this.sitePaths = sitePaths;
+    this.indexFactory = indexFactory;
+    this.indexes = indexes;
+    this.reindexerFactory = reindexerFactory;
+  }
+
+  @Override
+  public void start() {
+    FileBasedConfig cfg;
+    try {
+      cfg = loadGerritIndexConfig(sitePaths);
+    } catch (ConfigInvalidException e) {
+      throw fail(e);
+    } catch (IOException e) {
+      throw fail(e);
+    }
+
+    if (!sitePaths.index_dir.exists()) {
+      throw new ProvisionException("No index versions ready; run Reindex");
+    } else if (!sitePaths.index_dir.isDirectory()) {
+      log.warn("Not a directory: %s", sitePaths.index_dir.getAbsolutePath());
+      throw new ProvisionException("No index versions ready; run Reindex");
+    }
+
+    TreeMap<Integer, Version> versions = scanVersions(cfg);
+    // Search from the most recent ready version.
+    // Write to the most recent ready version and the most recent version.
+    Version search = null;
+    List<Version> write = Lists.newArrayListWithCapacity(2);
+    for (Version v : versions.descendingMap().values()) {
+      if (v.schema == null) {
+        continue;
+      }
+      if (write.isEmpty()) {
+        write.add(v);
+      }
+      if (v.ready) {
+        search = v;
+        if (!write.contains(v)) {
+          write.add(v);
+        }
+        break;
+      }
+    }
+    if (search == null) {
+      throw new ProvisionException("No index versions ready; run Reindex");
+    }
+
+    markNotReady(cfg, versions.values(), write);
+    LuceneChangeIndex searchIndex = indexFactory.create(search.schema, null);
+    indexes.setSearchIndex(searchIndex);
+    for (Version v : write) {
+      if (v.schema != null) {
+        if (v.version != search.version) {
+          indexes.addWriteIndex(indexFactory.create(v.schema, null));
+        } else {
+          indexes.addWriteIndex(searchIndex);
+        }
+      }
+    }
+
+    int latest = write.get(0).version;
+    if (latest != search.version) {
+      reindexerFactory.create(latest).start();
+    }
+  }
+
+  private TreeMap<Integer, Version> scanVersions(Config cfg) {
+    TreeMap<Integer, Version> versions = Maps.newTreeMap();
+    for (Schema<ChangeData> schema : ChangeSchemas.ALL.values()) {
+      File f = getDir(sitePaths, schema);
+      boolean exists = f.exists() && f.isDirectory();
+      if (f.exists() && !f.isDirectory()) {
+        log.warn("Not a directory: %s", f.getAbsolutePath());
+      }
+      int v = schema.getVersion();
+      versions.put(v, new Version(schema, v, exists, getReady(cfg, v)));
+    }
+
+    for (File f : sitePaths.index_dir.listFiles()) {
+      if (!f.getName().startsWith(CHANGES_PREFIX)) {
+        continue;
+      }
+      String versionStr = f.getName().substring(CHANGES_PREFIX.length());
+      Integer v = Ints.tryParse(versionStr);
+      if (v == null || versionStr.length() != 4) {
+        log.warn("Unrecognized version in index directory: {}",
+            f.getAbsolutePath());
+        continue;
+      }
+      if (!versions.containsKey(v)) {
+        versions.put(v, new Version(null, v, true, getReady(cfg, v)));
+      }
+    }
+    return versions;
+  }
+
+  private void markNotReady(FileBasedConfig cfg, Iterable<Version> versions,
+      Collection<Version> inUse) {
+    boolean dirty = false;
+    for (Version v : versions) {
+      if (!inUse.contains(v) && v.exists) {
+        setReady(cfg, v.version, false);
+        dirty = true;
+      }
+    }
+    if (dirty) {
+      try {
+        cfg.save();
+      } catch (IOException e) {
+        throw fail(e);
+      }
+    }
+  }
+
+  private ProvisionException fail(Throwable t) {
+    ProvisionException e = new ProvisionException("Error scanning indexes");
+    e.initCause(t);
+    throw e;
+  }
+
+  @Override
+  public void stop() {
+    // Do nothing; indexes are closed on demand by IndexCollection.
+  }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
new file mode 100644
index 0000000..d3dc963
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
@@ -0,0 +1,109 @@
+// 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.lucene;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.index.ChangeBatchIndexer;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+public class OnlineReindexer {
+  private static final Logger log = LoggerFactory
+      .getLogger(OnlineReindexer.class);
+
+  public interface Factory {
+    OnlineReindexer create(int version);
+  }
+
+  private final IndexCollection indexes;
+  private final ChangeBatchIndexer batchIndexer;
+  private final ProjectCache projectCache;
+  private final int version;
+
+  @Inject
+  OnlineReindexer(
+      IndexCollection indexes,
+      ChangeBatchIndexer batchIndexer,
+      ProjectCache projectCache,
+      @Assisted int version) {
+    this.indexes = indexes;
+    this.batchIndexer = batchIndexer;
+    this.projectCache = projectCache;
+    this.version = version;
+  }
+
+  public void start() {
+    Thread t = new Thread() {
+      @Override
+      public void run() {
+        reindex();
+      }
+    };
+    t.setName(String.format("Reindex v%d-v%d",
+        version(indexes.getSearchIndex()), version));
+    t.start();
+  }
+
+  private static int version(ChangeIndex i) {
+    return i.getSchema().getVersion();
+  }
+
+  private void reindex() {
+    ChangeIndex index = checkNotNull(indexes.getWriteIndex(version),
+        "not an active write schema version: %s", version);
+    log.info("Starting online reindex from schema version {} to {}",
+        version(indexes.getSearchIndex()), version(index));
+    ChangeBatchIndexer.Result result = batchIndexer.indexAll(
+        index, projectCache.all(), -1, -1, null, null);
+    if (!result.success()) {
+      log.error("Online reindex of schema version {} failed", version(index));
+      return;
+    }
+
+    indexes.setSearchIndex(index);
+    log.info("Reindex complete, using schema version {}", version(index));
+    try {
+      index.markReady(true);
+    } catch (IOException e) {
+      log.warn("Error activating new schema version {}", version(index));
+    }
+
+    List<ChangeIndex> toRemove = Lists.newArrayListWithExpectedSize(1);
+    for (ChangeIndex i : indexes.getWriteIndexes()) {
+      if (version(i) != version(index)) {
+        toRemove.add(i);
+      }
+    }
+    for (ChangeIndex i : toRemove) {
+      try {
+        i.markReady(false);
+        indexes.removeWriteIndex(version(i));
+      } catch (IOException e) {
+        log.warn("Error deactivating old schema version {}", version(i));
+      }
+    }
+  }
+}
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
new file mode 100644
index 0000000..f99cce8
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -0,0 +1,238 @@
+// 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.lucene;
+
+import static org.apache.lucene.search.BooleanClause.Occur.MUST;
+import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
+import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.SortKeyPredicate;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.FuzzyQuery;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.RegexpQuery;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.NumericUtils;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+public class QueryBuilder {
+  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+
+  public static Term idTerm(ChangeData cd) {
+    return intTerm(ID_FIELD, cd.getId().get());
+  }
+
+  public static Query toQuery(Schema<ChangeData> schema, Predicate<ChangeData> p)
+      throws QueryParseException {
+    if (p instanceof AndPredicate) {
+      return and(schema, p);
+    } else if (p instanceof OrPredicate) {
+      return or(schema, p);
+    } else if (p instanceof NotPredicate) {
+      return not(schema, p);
+    } else if (p instanceof IndexPredicate) {
+      return fieldQuery(schema, (IndexPredicate<ChangeData>) p);
+    } else {
+      throw new QueryParseException("cannot create query for index: " + p);
+    }
+  }
+
+  private static Query or(Schema<ChangeData> schema, Predicate<ChangeData> p)
+      throws QueryParseException {
+    try {
+      BooleanQuery q = new BooleanQuery();
+      for (int i = 0; i < p.getChildCount(); i++) {
+        q.add(toQuery(schema, p.getChild(i)), SHOULD);
+      }
+      return q;
+    } catch (BooleanQuery.TooManyClauses e) {
+      throw new QueryParseException("cannot create query for index: " + p, e);
+    }
+  }
+
+  private static Query and(Schema<ChangeData> schema, Predicate<ChangeData> p)
+      throws QueryParseException {
+    try {
+      BooleanQuery b = new BooleanQuery();
+      List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
+      for (int i = 0; i < p.getChildCount(); i++) {
+        Predicate<ChangeData> c = p.getChild(i);
+        if (c instanceof NotPredicate) {
+          Predicate<ChangeData> n = c.getChild(0);
+          if (n instanceof TimestampRangePredicate) {
+            b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST);
+          } else {
+            not.add(toQuery(schema, n));
+          }
+        } else {
+          b.add(toQuery(schema, c), MUST);
+        }
+      }
+      for (Query q : not) {
+        b.add(q, MUST_NOT);
+      }
+      return b;
+    } catch (BooleanQuery.TooManyClauses e) {
+      throw new QueryParseException("cannot create query for index: " + p, e);
+    }
+  }
+
+  private static Query not(Schema<ChangeData> schema, Predicate<ChangeData> p)
+      throws QueryParseException {
+    Predicate<ChangeData> n = p.getChild(0);
+    if (n instanceof TimestampRangePredicate) {
+      return notTimestamp((TimestampRangePredicate<ChangeData>) n);
+    }
+
+    // Lucene does not support negation, start with all and subtract.
+    BooleanQuery q = new BooleanQuery();
+    q.add(new MatchAllDocsQuery(), MUST);
+    q.add(toQuery(schema, n), MUST_NOT);
+    return q;
+  }
+
+  private static Query fieldQuery(Schema<ChangeData> schema,
+      IndexPredicate<ChangeData> p) throws QueryParseException {
+    if (p.getType() == FieldType.INTEGER) {
+      return intQuery(p);
+    } else if (p.getType() == FieldType.TIMESTAMP) {
+      return timestampQuery(p);
+    } else if (p.getType() == FieldType.EXACT) {
+      return exactQuery(p);
+    } else if (p.getType() == FieldType.PREFIX) {
+      return prefixQuery(p);
+    } else if (p.getType() == FieldType.FULL_TEXT) {
+      return fullTextQuery(p);
+    } else if (p instanceof SortKeyPredicate) {
+      return sortKeyQuery(schema, (SortKeyPredicate) p);
+    } else {
+      throw badFieldType(p.getType());
+    }
+  }
+
+  private static Term intTerm(String name, int value) {
+    BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_INT);
+    NumericUtils.intToPrefixCodedBytes(value, 0, bytes);
+    return new Term(name, bytes);
+  }
+
+  private static Query intQuery(IndexPredicate<ChangeData> p)
+      throws QueryParseException {
+    int value;
+    try {
+      // Can't use IntPredicate because it and IndexPredicate are different
+      // subclasses of OperatorPredicate.
+      value = Integer.valueOf(p.getValue());
+    } catch (IllegalArgumentException e) {
+      throw new QueryParseException("not an integer: " + p.getValue());
+    }
+    return new TermQuery(intTerm(p.getField().getName(), value));
+  }
+
+  private static Query sortKeyQuery(Schema<ChangeData> schema, SortKeyPredicate p) {
+    long min = p.getMinValue(schema);
+    long max = p.getMaxValue(schema);
+    return NumericRangeQuery.newLongRange(
+        p.getField().getName(),
+        min != Long.MIN_VALUE ? min : null,
+        max != Long.MAX_VALUE ? max : null,
+        false, false);
+  }
+
+  private static Query timestampQuery(IndexPredicate<ChangeData> p)
+      throws QueryParseException {
+    if (p instanceof TimestampRangePredicate) {
+      TimestampRangePredicate<ChangeData> r =
+          (TimestampRangePredicate<ChangeData>) p;
+      return NumericRangeQuery.newIntRange(
+          r.getField().getName(),
+          toIndexTime(r.getMinTimestamp()),
+          toIndexTime(r.getMaxTimestamp()),
+          true, true);
+    }
+    throw new QueryParseException("not a timestamp: " + p);
+  }
+
+  private static Query notTimestamp(TimestampRangePredicate<ChangeData> r)
+      throws QueryParseException {
+    if (r.getMinTimestamp().getTime() == 0) {
+      return NumericRangeQuery.newIntRange(
+          r.getField().getName(),
+          toIndexTime(r.getMaxTimestamp()),
+          null,
+          true, true);
+    }
+    throw new QueryParseException("cannot negate: " + r);
+  }
+
+  private static Query exactQuery(IndexPredicate<ChangeData> p) {
+    if (p instanceof RegexPredicate<?>) {
+      return regexQuery(p);
+    } else {
+      return new TermQuery(new Term(p.getField().getName(), p.getValue()));
+    }
+  }
+
+  private static Query regexQuery(IndexPredicate<ChangeData> p) {
+    String re = p.getValue();
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+    return new RegexpQuery(new Term(p.getField().getName(), re));
+  }
+
+  private static Query prefixQuery(IndexPredicate<ChangeData> p) {
+    return new PrefixQuery(new Term(p.getField().getName(), p.getValue()));
+  }
+
+  private static Query fullTextQuery(IndexPredicate<ChangeData> p) {
+    return new FuzzyQuery(new Term(p.getField().getName(), p.getValue()));
+  }
+
+  public static int toIndexTime(Timestamp ts) {
+    return (int) (ts.getTime() / 60000);
+  }
+
+  public static IllegalArgumentException badFieldType(FieldType<?> t) {
+    return new IllegalArgumentException("unknown index field type " + t);
+  }
+
+  private QueryBuilder() {
+  }
+}
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
new file mode 100644
index 0000000..655581d
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
@@ -0,0 +1,208 @@
+// 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.lucene;
+
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.TrackingIndexWriter;
+import org.apache.lucene.search.ControlledRealTimeReopenThread;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.ReferenceManager.RefreshListener;
+import org.apache.lucene.search.SearcherFactory;
+import org.apache.lucene.search.SearcherManager;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Piece of the change index that is implemented as a separate Lucene index. */
+class SubIndex {
+  private static final Logger log = LoggerFactory.getLogger(SubIndex.class);
+
+  private final Directory dir;
+  private final TrackingIndexWriter writer;
+  private final SearcherManager searcherManager;
+  private final ControlledRealTimeReopenThread<IndexSearcher> reopenThread;
+  private final ConcurrentMap<RefreshListener, Boolean> refreshListeners;
+
+  SubIndex(File file, IndexWriterConfig writerConfig) throws IOException {
+    this(FSDirectory.open(file), file.getName(), writerConfig);
+  }
+
+  SubIndex(Directory dir, String dirName, IndexWriterConfig writerConfig)
+      throws IOException {
+    this.dir = dir;
+    writer = new TrackingIndexWriter(new IndexWriter(dir, writerConfig));
+    searcherManager = new SearcherManager(
+        writer.getIndexWriter(), true, new SearcherFactory());
+
+    refreshListeners = Maps.newConcurrentMap();
+    searcherManager.addListener(new RefreshListener() {
+      @Override
+      public void beforeRefresh() throws IOException {
+      }
+
+      @Override
+      public void afterRefresh(boolean didRefresh) throws IOException {
+        for (RefreshListener l : refreshListeners.keySet()) {
+          l.afterRefresh(didRefresh);
+        }
+      }
+    });
+
+    reopenThread = new ControlledRealTimeReopenThread<IndexSearcher>(
+        writer, searcherManager,
+        0.500 /* maximum stale age (seconds) */,
+        0.010 /* minimum stale age (seconds) */);
+    reopenThread.setName("NRT " + dirName);
+    reopenThread.setPriority(Math.min(
+        Thread.currentThread().getPriority() + 2,
+        Thread.MAX_PRIORITY));
+    reopenThread.setDaemon(true);
+    reopenThread.start();
+  }
+
+  void close() {
+    reopenThread.close();
+    try {
+      writer.getIndexWriter().commit();
+      writer.getIndexWriter().close(true);
+    } catch (IOException e) {
+      log.warn("error closing Lucene writer", e);
+    }
+    try {
+      dir.close();
+    } catch (IOException e) {
+      log.warn("error closing Lucene directory", e);
+    }
+  }
+
+  ListenableFuture<?> insert(Document doc) throws IOException {
+    return new NrtFuture(writer.addDocument(doc));
+  }
+
+  ListenableFuture<?> replace(Term term, Document doc) throws IOException {
+    return new NrtFuture(writer.updateDocument(term, doc));
+  }
+
+  ListenableFuture<?> delete(Term term) throws IOException {
+    return new NrtFuture(writer.deleteDocuments(term));
+  }
+
+  void deleteAll() throws IOException {
+    writer.deleteAll();
+  }
+
+  IndexSearcher acquire() throws IOException {
+    return searcherManager.acquire();
+  }
+
+  void release(IndexSearcher searcher) throws IOException {
+    searcherManager.release(searcher);
+  }
+
+  private final class NrtFuture extends AbstractFuture<Void>
+      implements RefreshListener {
+    private final long gen;
+    private final AtomicBoolean hasListeners = new AtomicBoolean();
+
+    NrtFuture(long gen) {
+      this.gen = gen;
+    }
+
+    @Override
+    public Void get() throws InterruptedException, ExecutionException {
+      if (!isDone()) {
+        reopenThread.waitForGeneration(gen);
+        set(null);
+      }
+      return super.get();
+    }
+
+    @Override
+    public Void get(long timeout, TimeUnit unit) throws InterruptedException,
+        TimeoutException, ExecutionException {
+      if (!isDone()) {
+        reopenThread.waitForGeneration(gen,
+            (int) TimeUnit.MILLISECONDS.convert(timeout, unit));
+        set(null);
+      }
+      return super.get(timeout, unit);
+    }
+
+    @Override
+    public boolean isDone() {
+      if (super.isDone()) {
+        return true;
+      } else if (isSearcherCurrent()) {
+        set(null);
+        return true;
+      }
+      return false;
+    }
+
+    @Override
+    public void addListener(Runnable listener, Executor executor) {
+      if (hasListeners.compareAndSet(false, true) && !isDone()) {
+        searcherManager.addListener(this);
+      }
+      super.addListener(listener, executor);
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+      if (hasListeners.get()) {
+        refreshListeners.put(this, true);
+      }
+      return super.cancel(mayInterruptIfRunning);
+    }
+
+    @Override
+    public void beforeRefresh() throws IOException {
+    }
+
+    @Override
+    public void afterRefresh(boolean didRefresh) throws IOException {
+      if (isSearcherCurrent()) {
+        refreshListeners.remove(this);
+        set(null);
+      }
+    }
+
+    private boolean isSearcherCurrent() {
+      try {
+        return reopenThread.waitForGeneration(gen, 0);
+      } catch (InterruptedException e) {
+        log.warn("Interrupted waiting for searcher generation", e);
+        return false;
+      }
+    }
+  }
+}
diff --git a/gerrit-main/BUCK b/gerrit-main/BUCK
new file mode 100644
index 0000000..da39eec
--- /dev/null
+++ b/gerrit-main/BUCK
@@ -0,0 +1,13 @@
+java_binary(
+  name = 'main_bin',
+  main_class = 'Main',
+  deps = [':main_lib'],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'main_lib',
+  srcs = ['src/main/java/Main.java'],
+  deps = ['//gerrit-launcher:launcher'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
deleted file mode 100644
index 46b0a9f..0000000
--- a/gerrit-main/pom.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-main</artifactId>
-  <name>Gerrit Code Review - Main</name>
-
-  <description>
-    Main class to bootstrap out of a WAR
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-launcher</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <configuration>
-          <source>1.2</source>
-          <target>1.2</target>
-          <encoding>UTF-8</encoding>
-        </configuration>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-shade-plugin</artifactId>
-        <executions>
-          <execution>
-            <phase>package</phase>
-            <goals>
-              <goal>shade</goal>
-            </goals>
-            <configuration>
-              <createDependencyReducedPom>false</createDependencyReducedPom>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK
new file mode 100644
index 0000000..8da83c3
--- /dev/null
+++ b/gerrit-openid/BUCK
@@ -0,0 +1,22 @@
+java_library2(
+  name = 'openid',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/resources/**/*']),
+  deps = [
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-gwtexpui:server',
+    '//gerrit-httpd:httpd',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+    '//lib/openid:consumer',
+  ],
+  compile_deps = ['//lib:servlet-api-3_0'],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
deleted file mode 100644
index 1e73867..0000000
--- a/gerrit-openid/pom.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-openid</artifactId>
-  <name>Gerrit Code Review - OpenID servlet and RPC</name>
-
-  <description>
-    OpenID
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>servlet-api</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.code.findbugs</groupId>
-      <artifactId>jsr305</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit.http.server</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.openid4java</groupId>
-      <artifactId>openid4java</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-httpd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-server</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
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 678ec00..c19d74b 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
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.extensions.restapi.Url;
@@ -42,7 +43,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import javax.annotation.Nullable;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServlet;
@@ -79,7 +79,7 @@
         "openid", "maxRedirectUrlLength",
         10);
 
-    if (Strings.isNullOrEmpty(urlProvider.get())) {
+    if (urlProvider == null || Strings.isNullOrEmpty(urlProvider.get())) {
       log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
     }
 
@@ -229,7 +229,7 @@
   private void sendForm(HttpServletRequest req, HttpServletResponse res,
       boolean link, @Nullable String errorMessage) throws IOException {
     String self = req.getRequestURI();
-    String cancel = Objects.firstNonNull(urlProvider.get(), "/");
+    String cancel = Objects.firstNonNull(urlProvider != null ? urlProvider.get() : "/", "/");
     String token = getToken(req);
     if (!token.equals("/")) {
       cancel += "#" + token;
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 5d74166..5817a55 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -208,7 +208,7 @@
     }
   }
 
-  /** Called by {@link OpenIdLoginForm} doGet, doPost */
+  /** Called by {@link OpenIdLoginServlet} doGet, doPost */
   void doAuth(final HttpServletRequest req, final HttpServletResponse rsp)
       throws Exception {
     if (OMODE_CANCEL.equals(req.getParameter(OPENID_MODE))) {
diff --git a/gerrit-patch-commonsnet/BUCK b/gerrit-patch-commonsnet/BUCK
new file mode 100644
index 0000000..53b382f
--- /dev/null
+++ b/gerrit-patch-commonsnet/BUCK
@@ -0,0 +1,11 @@
+java_library(
+  name = 'commons-net',
+  srcs = glob(['src/main/java/org/apache/commons/net/**/*.java']),
+  deps = [
+    '//gerrit-util-ssl:ssl',
+    '//lib/commons:codec',
+    '//lib/commons:net',
+    '//lib/log:api',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
deleted file mode 100644
index 82c1d82..0000000
--- a/gerrit-patch-commonsnet/pom.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-patch-commonsnet</artifactId>
-  <name>Gerrit Code Review - Patch commons-net</name>
-
-  <description>
-    Hacks to expose package-private data from commons-net to Gerrit
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>commons-net</groupId>
-      <artifactId>commons-net</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>commons-codec</groupId>
-      <artifactId>commons-codec</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-api</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-util-ssl</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/gerrit-patch-jgit/BUCK b/gerrit-patch-jgit/BUCK
new file mode 100644
index 0000000..18890ac
--- /dev/null
+++ b/gerrit-patch-jgit/BUCK
@@ -0,0 +1,32 @@
+SRC = 'src/main/java/org/eclipse/jgit/'
+
+gwt_module(
+  name = 'client',
+  srcs = [
+    SRC + 'diff/Edit_JsonSerializer.java',
+    SRC + 'diff/ReplaceEdit.java',
+  ],
+  gwtxml = SRC + 'JGit.gwt.xml',
+  deps = [
+    '//lib:gwtjsonrpc',
+    '//lib/gwt:user',
+    '//lib/jgit:jgit',
+    '//lib/jgit:Edit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'server',
+  srcs = [
+    SRC + 'diff/EditDeserializer.java',
+    SRC + 'diff/ReplaceEdit.java',
+    SRC + 'internal/storage/file/WindowCacheStatAccessor.java',
+    SRC + 'lib/ObjectIdSerialization.java',
+  ],
+  deps = [
+    '//lib:gson',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
deleted file mode 100644
index a1b85b3..0000000
--- a/gerrit-patch-jgit/pom.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-patch-jgit</artifactId>
-  <name>Gerrit Code Review - Patch JGit</name>
-
-  <description>
-    Hacks to expose package-private data from JGit to Gerrit
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>gwtjsonrpc</groupId>
-      <artifactId>gwtjsonrpc</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-user</artifactId>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java
index f241daa..1e94050 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java
+++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java
@@ -14,8 +14,6 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
-import org.eclipse.jgit.internal.storage.file.WindowCache;
-
 // Hack to obtain visibility to package level methods only.
 // These aren't yet part of the public JGit API.
 
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
new file mode 100644
index 0000000..8915353
--- /dev/null
+++ b/gerrit-pgm/BUCK
@@ -0,0 +1,141 @@
+SRCS = 'src/main/java/com/google/gerrit/pgm/'
+
+INIT_API_SRCS = [SRCS + n for n in [
+  'init/InitFlags.java',
+  'init/InitStep.java',
+  'init/InitStep.java',
+  'init/InstallPlugins.java',
+  'init/Section.java',
+  'util/ConsoleUI.java',
+  'util/Die.java',
+]]
+
+java_library(
+  name = 'init-api',
+  srcs = INIT_API_SRCS,
+  deps = [
+    '//gerrit-common:server',
+    '//gerrit-server:server',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'init-api-src',
+  srcs = INIT_API_SRCS,
+  visibility = ['PUBLIC'],
+)
+
+INIT_BASE_SRCS = [SRCS + 'BaseInit.java'] + glob(
+    [SRCS + n for n in [
+      'init/**/*.java',
+      'util/**/*.java',
+    ]],
+    excludes = INIT_API_SRCS +
+      [SRCS + n for n in [
+        'init/Browser.java',
+        'util/ErrorLogFile.java',
+        'util/GarbageCollectionLogFile.java',
+        'util/LogFileCompressor.java',
+        'util/RuntimeShutdown.java',
+      ]]
+  )
+
+INIT_BASE_RSRCS = ['src/main/resources/com/google/gerrit/pgm/libraries.config']
+
+java_library2(
+  name = 'init-base',
+  srcs = INIT_BASE_SRCS,
+  resources = INIT_BASE_RSRCS,
+  deps = [
+    ':init-api',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-util-cli:cli',
+    '//lib/commons:dbcp',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/jgit:jgit',
+    '//lib/mina:sshd',
+    '//lib:args4j',
+    '//lib:guava',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+  ],
+  compile_deps = ['//gerrit-launcher:launcher'],
+  visibility = [
+    '//gerrit-war:',
+    '//gerrit-acceptance-tests/...',
+  ],
+)
+
+java_library2(
+  name = 'pgm',
+  srcs = glob(
+    ['src/main/java/**/*.java'],
+    excludes = INIT_API_SRCS + INIT_BASE_SRCS
+  ),
+  resources = glob(
+    ['src/main/resources/**/*'],
+    excludes = INIT_BASE_RSRCS),
+  deps = [
+    ':init-api',
+    ':init-base',
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-gwtexpui:server',
+    '//gerrit-httpd:httpd',
+    '//gerrit-lucene:lucene',
+    '//gerrit-openid:openid',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-server/src/main/prolog:common',
+    '//gerrit-solr:solr',
+    '//gerrit-sshd:sshd',
+    '//gerrit-util-cli:cli',
+    '//lib:args4j',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:h2',
+    '//lib:servlet-api-3_0',
+    '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
+    '//lib/jetty:server',
+    '//lib/jetty:servlet',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+    '//lib/log:log4j',
+    '//lib/lucene:core',
+    '//lib/prolog:prolog-cafe',
+  ],
+  compile_deps = ['//gerrit-launcher:launcher'],
+  visibility = [
+    '//:',
+    '//gerrit-acceptance-tests/...',
+    '//tools/eclipse:classpath',
+    '//Documentation:licenses.txt',
+  ],
+)
+
+java_test(
+  name = 'pgm_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':init-api',
+    ':init-base',
+    ':pgm',
+    '//gerrit-server:server',
+    '//lib:junit',
+    '//lib:easymock',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/jgit:junit',
+  ],
+  source_under_test = [':pgm'],
+)
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
deleted file mode 100644
index 9acb912..0000000
--- a/gerrit-pgm/pom.xml
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-pgm</artifactId>
-  <name>Gerrit Code Review - Pgm</name>
-
-  <description>
-    Command line executables
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.h2database</groupId>
-      <artifactId>h2</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>postgresql</groupId>
-      <artifactId>postgresql</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-main</artifactId>
-      <version>${project.version}</version>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-util-cli</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-server</artifactId>
-      <version>${project.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>org.apache.tomcat</groupId>
-          <artifactId>servlet-api</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-openid</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-sshd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-httpd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jetty</groupId>
-      <artifactId>jetty-servlet</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>tomcat-servlet-api</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit.junit</artifactId>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
new file mode 100644
index 0000000..bd22146
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
@@ -0,0 +1,299 @@
+// 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.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
+import static com.google.inject.Stage.PRODUCTION;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.google.gerrit.pgm.init.InitFlags;
+import com.google.gerrit.pgm.init.InitModule;
+import com.google.gerrit.pgm.init.InstallPlugins;
+import com.google.gerrit.pgm.init.SitePathInitializer;
+import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.util.Die;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.schema.SchemaUpdater;
+import com.google.gerrit.server.schema.UpdateUI;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.AbstractModule;
+import com.google.inject.CreationException;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.spi.Message;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+/** Initialize a new Gerrit installation. */
+public class BaseInit extends SiteProgram {
+
+  private final boolean standalone;
+
+  public BaseInit() {
+    this.standalone = true;
+  }
+
+  public BaseInit(File sitePath, boolean standalone) {
+    this(sitePath, null, standalone);
+  }
+
+  public BaseInit(File sitePath, final Provider<DataSource> dsProvider,
+      boolean standalone) {
+    super(sitePath, dsProvider);
+    this.standalone = standalone;
+  }
+
+  @Override
+  public int run() throws Exception {
+    final SiteInit init = createSiteInit();
+    if (beforeInit(init)) {
+      return 0;
+    }
+
+    init.flags.autoStart = getAutoStart() && init.site.isNew;
+    init.flags.skipPlugins = skipPlugins();
+
+    final SiteRun run;
+    try {
+      init.initializer.run();
+      init.flags.deleteOnFailure = false;
+
+      run = createSiteRun(init);
+      run.upgradeSchema();
+    } catch (Exception failure) {
+      if (init.flags.deleteOnFailure) {
+        recursiveDelete(getSitePath());
+      }
+      throw failure;
+    } catch (Error failure) {
+      if (init.flags.deleteOnFailure) {
+        recursiveDelete(getSitePath());
+      }
+      throw failure;
+    }
+
+    System.err.println("Initialized " + getSitePath().getCanonicalPath());
+    afterInit(run);
+    return 0;
+  }
+
+  protected boolean skipPlugins() {
+    return false;
+  }
+
+  protected boolean beforeInit(SiteInit init) throws Exception {
+    return false;
+  }
+
+  protected void afterInit(SiteRun run) throws Exception {
+  }
+
+  protected List<String> getInstallPlugins() {
+    return null;
+  }
+
+  protected boolean getAutoStart() {
+    return false;
+  }
+
+  static class SiteInit {
+    final SitePaths site;
+    final InitFlags flags;
+    final ConsoleUI ui;
+    final SitePathInitializer initializer;
+
+    @Inject
+    SiteInit(final SitePaths site, final InitFlags flags, final ConsoleUI ui,
+        final SitePathInitializer initializer) {
+      this.site = site;
+      this.flags = flags;
+      this.ui = ui;
+      this.initializer = initializer;
+    }
+  }
+
+  private SiteInit createSiteInit() {
+    final ConsoleUI ui = getConsoleUI();
+    final File sitePath = getSitePath();
+    final List<Module> m = new ArrayList<Module>();
+
+    m.add(new InitModule(standalone));
+    m.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ConsoleUI.class).toInstance(ui);
+        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        List<String> plugins =
+            Objects.firstNonNull(getInstallPlugins(), Lists.<String> newArrayList());
+        bind(new TypeLiteral<List<String>>() {}).annotatedWith(
+            InstallPlugins.class).toInstance(plugins);
+      }
+    });
+
+    try {
+      return Guice.createInjector(PRODUCTION, m).getInstance(SiteInit.class);
+    } catch (CreationException ce) {
+      final Message first = ce.getErrorMessages().iterator().next();
+      Throwable why = first.getCause();
+
+      if (why instanceof Die) {
+        throw (Die) why;
+      }
+
+      final StringBuilder buf = new StringBuilder(ce.getMessage());
+      while (why != null) {
+        buf.append("\n");
+        buf.append(why.getMessage());
+        why = why.getCause();
+        if (why != null) {
+          buf.append("\n  caused by ");
+        }
+      }
+      throw die(buf.toString(), new RuntimeException("InitInjector failed", ce));
+    }
+  }
+
+  protected ConsoleUI getConsoleUI() {
+    return ConsoleUI.getInstance(false);
+  }
+
+  static class SiteRun {
+    final ConsoleUI ui;
+    final SitePaths site;
+    final InitFlags flags;
+    final SchemaUpdater schemaUpdater;
+    final SchemaFactory<ReviewDb> schema;
+    final GitRepositoryManager repositoryManager;
+
+    @Inject
+    SiteRun(final ConsoleUI ui, final SitePaths site, final InitFlags flags,
+        final SchemaUpdater schemaUpdater,
+        final SchemaFactory<ReviewDb> schema,
+        final GitRepositoryManager repositoryManager) {
+      this.ui = ui;
+      this.site = site;
+      this.flags = flags;
+      this.schemaUpdater = schemaUpdater;
+      this.schema = schema;
+      this.repositoryManager = repositoryManager;
+    }
+
+    void upgradeSchema() throws OrmException {
+      final List<String> pruneList = new ArrayList<String>();
+      schemaUpdater.update(new UpdateUI() {
+        @Override
+        public void message(String msg) {
+          System.err.println(msg);
+          System.err.flush();
+        }
+
+        @Override
+        public boolean yesno(boolean def, String msg) {
+          return ui.yesno(def, msg);
+        }
+
+        @Override
+        public boolean isBatch() {
+          return ui.isBatch();
+        }
+
+        @Override
+        public void pruneSchema(StatementExecutor e, List<String> prune) {
+          for (String p : prune) {
+            if (!pruneList.contains(p)) {
+              pruneList.add(p);
+            }
+          }
+        }
+      });
+
+      if (!pruneList.isEmpty()) {
+        StringBuilder msg = new StringBuilder();
+        msg.append("Execute the following SQL to drop unused objects:\n");
+        msg.append("\n");
+        for (String sql : pruneList) {
+          msg.append("  ");
+          msg.append(sql);
+          msg.append(";\n");
+        }
+
+        if (ui.isBatch()) {
+          System.err.print(msg);
+          System.err.flush();
+
+        } else if (ui.yesno(true, "%s\nExecute now", msg)) {
+          final JdbcSchema db = (JdbcSchema) schema.open();
+          try {
+            final JdbcExecutor e = new JdbcExecutor(db);
+            try {
+              for (String sql : pruneList) {
+                e.execute(sql);
+              }
+            } finally {
+              e.close();
+            }
+          } finally {
+            db.close();
+          }
+        }
+      }
+    }
+  }
+
+  private SiteRun createSiteRun(final SiteInit init) {
+    return createSysInjector(init).getInstance(SiteRun.class);
+  }
+
+  private Injector createSysInjector(final SiteInit init) {
+    final List<Module> modules = new ArrayList<Module>();
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ConsoleUI.class).toInstance(init.ui);
+        bind(InitFlags.class).toInstance(init.flags);
+      }
+    });
+    return createDbInjector(SINGLE_USER).createChildInjector(modules);
+  }
+
+  private static void recursiveDelete(File path) {
+    File[] entries = path.listFiles();
+    if (entries != null) {
+      for (File e : entries) {
+        recursiveDelete(e);
+      }
+    }
+    if (!path.delete() && path.exists()) {
+      System.err.println("warn: Cannot remove " + path);
+    }
+  }
+}
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 ca98a84..6adaf32 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
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.CacheBasedWebSession;
@@ -28,6 +30,7 @@
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.http.jetty.GetUserFilter;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
 import com.google.gerrit.pgm.http.jetty.JettyModule;
@@ -38,44 +41,40 @@
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.GerritGlobalModule;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
 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.NoIndexModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.patch.IntraLineWorkerPool;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
-import com.google.gerrit.server.plugins.PluginModule;
-import com.google.gerrit.server.schema.SchemaUpdater;
+import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
-import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.ssh.NoSshKeyCache;
 import com.google.gerrit.server.ssh.NoSshModule;
+import com.google.gerrit.solr.SolrIndexModule;
+import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.gerrit.sshd.commands.SlaveCommandModule;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
+import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Module;
 import com.google.inject.Provider;
+import com.google.inject.Stage;
 
-import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -121,6 +120,10 @@
   @Option(name = "--headless", usage = "Don't start the UI frontend")
   private boolean headless;
 
+  @Option(name = "--init", aliases = {"-i"},
+      usage = "Init site before starting the daemon")
+  private boolean doInit;
+
   private final LifecycleManager manager = new LifecycleManager();
   private Injector dbInjector;
   private Injector cfgInjector;
@@ -129,18 +132,27 @@
   private Injector webInjector;
   private Injector httpdInjector;
   private File runFile;
+  private boolean test;
 
   private Runnable serverStarted;
 
   public Daemon() {
   }
 
+  @VisibleForTesting
   public Daemon(Runnable serverStarted) {
     this.serverStarted = serverStarted;
   }
 
   @Override
   public int run() throws Exception {
+    if (doInit) {
+      try {
+        new Init(getSitePath()).run();
+      } catch (Exception e) {
+        throw die("Init failed", e);
+      }
+    }
     mustHaveValidSite();
     Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
       @Override
@@ -171,23 +183,7 @@
     }
 
     try {
-      dbInjector = createDbInjector(MULTI_USER);
-      cfgInjector = createCfgInjector();
-      sysInjector = createSysInjector();
-      sysInjector.getInstance(PluginGuiceEnvironment.class)
-        .setCfgInjector(cfgInjector);
-      sysInjector.getInstance(SchemaUpgrade.class).upgradeSchema();
-      manager.add(dbInjector, cfgInjector, sysInjector);
-
-      if (sshd) {
-        initSshd();
-      }
-
-      if (httpd) {
-        initHttpd();
-      }
-
-      manager.start();
+      start();
       RuntimeShutdown.add(new Runnable() {
         @Override
         public void run() {
@@ -228,73 +224,44 @@
     }
   }
 
-  static class SchemaUpgrade {
-
-    private final Config config;
-    private final SchemaUpdater updater;
-    private final SchemaFactory<ReviewDb> schema;
-
-    @Inject
-    SchemaUpgrade(@GerritServerConfig Config config, SchemaUpdater updater,
-        SchemaFactory<ReviewDb> schema) {
-      this.config = config;
-      this.updater = updater;
-      this.schema = schema;
-    }
-
-    void upgradeSchema() throws OrmException {
-      SchemaUpgradePolicy policy =
-          config.getEnum("site", null, "upgradeSchemaOnStartup",
-              SchemaUpgradePolicy.OFF);
-      if (policy == SchemaUpgradePolicy.AUTO
-          || policy == SchemaUpgradePolicy.AUTO_NO_PRUNE) {
-        final List<String> pruneList = new ArrayList<String>();
-        updater.update(new UpdateUI() {
-          @Override
-          public void message(String msg) {
-            log.info(msg);
-          }
-
-          @Override
-          public boolean yesno(boolean def, String msg) {
-            return true;
-          }
-
-          @Override
-          public boolean isBatch() {
-            return true;
-          }
-
-          @Override
-          public void pruneSchema(StatementExecutor e, List<String> prune) {
-            for (String p : prune) {
-              if (!pruneList.contains(p)) {
-                pruneList.add(p);
-              }
-            }
-          }
-        });
-
-        if (!pruneList.isEmpty() && policy == SchemaUpgradePolicy.AUTO) {
-          log.info("Pruning: " + pruneList.toString());
-          final JdbcSchema db = (JdbcSchema) schema.open();
-          try {
-            final JdbcExecutor e = new JdbcExecutor(db);
-            try {
-              for (String sql : pruneList) {
-                e.execute(sql);
-              }
-            } finally {
-              e.close();
-            }
-          } finally {
-            db.close();
-          }
-        }
-      }
-    }
+  @VisibleForTesting
+  public LifecycleManager getLifecycleManager() {
+    return manager;
   }
 
+  @VisibleForTesting
+  public void setDatabaseForTesting(List<Module> modules) {
+    dbInjector = Guice.createInjector(Stage.PRODUCTION, modules);
+    test = true;
+    headless = true;
+  }
+
+  @VisibleForTesting
+  public void start() {
+    if (dbInjector == null) {
+      dbInjector = createDbInjector(MULTI_USER);
+    }
+    cfgInjector = createCfgInjector();
+    sysInjector = createSysInjector();
+    sysInjector.getInstance(PluginGuiceEnvironment.class)
+      .setCfgInjector(cfgInjector);
+    manager.add(dbInjector, cfgInjector, sysInjector);
+
+    if (sshd) {
+      initSshd();
+    }
+
+    if (Objects.firstNonNull(httpd, true)) {
+      initHttpd();
+    }
+
+    manager.start();
+  }
+
+  @VisibleForTesting
+  public void stop() {
+    manager.stop();
+  }
 
   private String myVersion() {
     return com.google.gerrit.common.Version.getVersion();
@@ -315,11 +282,24 @@
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new PluginModule());
-    if (httpd) {
+    modules.add(new PluginRestApiModule());
+    AbstractModule changeIndexModule;
+    switch (IndexModule.getIndexType(cfgInjector)) {
+      case LUCENE:
+        changeIndexModule = new LuceneIndexModule();
+        break;
+      case SOLR:
+        changeIndexModule = new SolrIndexModule();
+        break;
+      default:
+        changeIndexModule = new NoIndexModule();
+    }
+    modules.add(changeIndexModule);
+    if (Objects.firstNonNull(httpd, true)) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
         protected Class<? extends Provider<String>> provider() {
@@ -362,6 +342,9 @@
     final List<Module> modules = new ArrayList<Module>();
     if (sshd) {
       modules.add(sysInjector.getInstance(SshModule.class));
+      if (!test) {
+        modules.add(new SshHostKeyModule());
+      }
       if (slave) {
         modules.add(new SlaveCommandModule());
       } else {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 3c7822c..68e0f6a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -14,296 +14,191 @@
 
 package com.google.gerrit.pgm;
 
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
-import static com.google.inject.Stage.PRODUCTION;
-
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.pgm.init.Browser;
-import com.google.gerrit.pgm.init.InitFlags;
-import com.google.gerrit.pgm.init.InitModule;
-import com.google.gerrit.pgm.init.SitePathInitializer;
+import com.google.gerrit.pgm.init.InitPlugins;
+import com.google.gerrit.pgm.init.InitPlugins.PluginData;
 import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.pgm.util.Die;
 import com.google.gerrit.pgm.util.ErrorLogFile;
 import com.google.gerrit.pgm.util.IoUtil;
-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;
-import com.google.gerrit.server.schema.SchemaUpdater;
-import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.util.HostPlatform;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.AbstractModule;
-import com.google.inject.CreationException;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
 import com.google.inject.Module;
-import com.google.inject.spi.Message;
+import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Option;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
+import java.util.ArrayList;
+
+import javax.sql.DataSource;
 
 /** Initialize a new Gerrit installation. */
-public class Init extends SiteProgram {
+public class Init extends BaseInit {
   @Option(name = "--batch", usage = "Batch mode; skip interactive prompting")
   private boolean batchMode;
 
   @Option(name = "--no-auto-start", usage = "Don't automatically start daemon after init")
   private boolean noAutoStart;
 
+  @Option(name = "--skip-plugins", usage = "Don't install plugin")
+  private boolean skipPlugins = false;
+
+  @Option(name = "--list-plugins", usage = "List available plugins")
+  private boolean listPlugins;
+
+  @Option(name = "--install-plugin", usage = "Install given plugin without asking", multiValued = true)
+  private List<String> installPlugins;
+
+  @Inject
+  Browser browser;
+
+  public Init() {
+  }
+
+  public Init(File sitePath) {
+    this(sitePath, null);
+  }
+
+  public Init(File sitePath, final Provider<DataSource> dsProvider) {
+    super(sitePath, dsProvider, true);
+    batchMode = true;
+    noAutoStart = true;
+  }
+
   @Override
-  public int run() throws Exception {
+  protected boolean beforeInit(SiteInit init) throws Exception {
     ErrorLogFile.errorOnlyConsole();
 
-    final SiteInit init = createSiteInit();
-    init.flags.autoStart = !noAutoStart && init.site.isNew;
-
-    final SiteRun run;
-    try {
-      init.initializer.run();
-      init.flags.deleteOnFailure = false;
-
-      run = createSiteRun(init);
-      run.upgradeSchema();
-    } catch (Exception failure) {
-      if (init.flags.deleteOnFailure) {
-        recursiveDelete(getSitePath());
-      }
-      throw failure;
-    } catch (Error failure) {
-      if (init.flags.deleteOnFailure) {
-        recursiveDelete(getSitePath());
-      }
-      throw failure;
-    }
-
-    System.err.println("Initialized " + getSitePath().getCanonicalPath());
-    run.start();
-    return 0;
-  }
-
-  static class SiteInit {
-    final SitePaths site;
-    final InitFlags flags;
-    final ConsoleUI ui;
-    final SitePathInitializer initializer;
-
-    @Inject
-    SiteInit(final SitePaths site, final InitFlags flags, final ConsoleUI ui,
-        final SitePathInitializer initializer) {
-      this.site = site;
-      this.flags = flags;
-      this.ui = ui;
-      this.initializer = initializer;
-    }
-  }
-
-  private SiteInit createSiteInit() {
-    final ConsoleUI ui = ConsoleUI.getInstance(batchMode);
-    final File sitePath = getSitePath();
-    final List<Module> m = new ArrayList<Module>();
-
-    m.add(new InitModule());
-    m.add(new AbstractModule() {
-      @Override
-      protected void configure() {
-        bind(ConsoleUI.class).toInstance(ui);
-        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
-      }
-    });
-
-    try {
-      return Guice.createInjector(PRODUCTION, m).getInstance(SiteInit.class);
-    } catch (CreationException ce) {
-      final Message first = ce.getErrorMessages().iterator().next();
-      Throwable why = first.getCause();
-
-      if (why instanceof Die) {
-        throw (Die) why;
-      }
-
-      final StringBuilder buf = new StringBuilder(ce.getMessage());
-      while (why != null) {
-        buf.append("\n");
-        buf.append(why.getMessage());
-        why = why.getCause();
-        if (why != null) {
-          buf.append("\n  caused by ");
-        }
-      }
-      throw die(buf.toString(), new RuntimeException("InitInjector failed", ce));
-    }
-  }
-
-  static class SiteRun {
-    final ConsoleUI ui;
-    final SitePaths site;
-    final InitFlags flags;
-    final SchemaUpdater schemaUpdater;
-    final SchemaFactory<ReviewDb> schema;
-    final GitRepositoryManager repositoryManager;
-    final Browser browser;
-
-    @Inject
-    SiteRun(final ConsoleUI ui, final SitePaths site, final InitFlags flags,
-        final SchemaUpdater schemaUpdater,
-        final SchemaFactory<ReviewDb> schema,
-        final GitRepositoryManager repositoryManager,
-        final Browser browser) {
-      this.ui = ui;
-      this.site = site;
-      this.flags = flags;
-      this.schemaUpdater = schemaUpdater;
-      this.schema = schema;
-      this.repositoryManager = repositoryManager;
-      this.browser = browser;
-    }
-
-    void upgradeSchema() throws OrmException {
-      final List<String> pruneList = new ArrayList<String>();
-      schemaUpdater.update(new UpdateUI() {
-        @Override
-        public void message(String msg) {
-          System.err.println(msg);
-          System.err.flush();
-        }
-
-        @Override
-        public boolean yesno(boolean def, String msg) {
-          return ui.yesno(def, msg);
-        }
-
-        @Override
-        public boolean isBatch() {
-          return ui.isBatch();
-        }
-
-        @Override
-        public void pruneSchema(StatementExecutor e, List<String> prune) {
-          for (String p : prune) {
-            if (!pruneList.contains(p)) {
-              pruneList.add(p);
-            }
+    if (!skipPlugins) {
+      final List<PluginData> plugins =
+          InitPlugins.listPluginsAndRemoveTempFiles(init.site);
+      ConsoleUI ui = ConsoleUI.getInstance(false);
+      verifyInstallPluginList(ui, plugins);
+      if (listPlugins) {
+        if (!plugins.isEmpty()) {
+          ui.message("Available plugins:\n");
+          for (PluginData plugin : plugins) {
+            ui.message(" * %s version %s\n", plugin.name, plugin.version);
           }
-        }
-      });
-
-      if (!pruneList.isEmpty()) {
-        StringBuilder msg = new StringBuilder();
-        msg.append("Execute the following SQL to drop unused objects:\n");
-        msg.append("\n");
-        for (String sql : pruneList) {
-          msg.append("  ");
-          msg.append(sql);
-          msg.append(";\n");
-        }
-
-        if (ui.isBatch()) {
-          System.err.print(msg);
-          System.err.flush();
-
-        } else if (ui.yesno(true, "%s\nExecute now", msg)) {
-          final JdbcSchema db = (JdbcSchema) schema.open();
-          try {
-            final JdbcExecutor e = new JdbcExecutor(db);
-            try {
-              for (String sql : pruneList) {
-                e.execute(sql);
-              }
-            } finally {
-              e.close();
-            }
-          } finally {
-            db.close();
-          }
-        }
-      }
-    }
-
-    void start() throws Exception {
-      if (flags.autoStart) {
-        if (HostPlatform.isWin32()) {
-          System.err.println("Automatic startup not supported on Win32.");
-
         } else {
-          startDaemon();
-          if (!ui.isBatch()) {
-            browser.open(PageLinks.ADMIN_PROJECTS);
-          }
+          ui.message("No plugins found.\n");
         }
+        return true;
       }
     }
-
-    void startDaemon() {
-      final String[] argv = {site.gerrit_sh.getAbsolutePath(), "start"};
-      final Process proc;
-      try {
-        System.err.println("Executing " + argv[0] + " " + argv[1]);
-        proc = Runtime.getRuntime().exec(argv);
-      } catch (IOException e) {
-        System.err.println("error: cannot start Gerrit: " + e.getMessage());
-        return;
-      }
-
-      try {
-        proc.getOutputStream().close();
-      } catch (IOException e) {
-      }
-
-      IoUtil.copyWithThread(proc.getInputStream(), System.err);
-      IoUtil.copyWithThread(proc.getErrorStream(), System.err);
-
-      for (;;) {
-        try {
-          final int rc = proc.waitFor();
-          if (rc != 0) {
-            System.err.println("error: cannot start Gerrit: exit status " + rc);
-          }
-          break;
-        } catch (InterruptedException e) {
-          // retry
-        }
-      }
-    }
-
+    return false;
   }
 
-  private SiteRun createSiteRun(final SiteInit init) {
-    return createSysInjector(init).getInstance(SiteRun.class);
-  }
-
-  private Injector createSysInjector(final SiteInit init) {
-    final List<Module> modules = new ArrayList<Module>();
+  @Override
+  protected void afterInit(SiteRun run) throws Exception {
+    List<Module> modules = Lists.newArrayList();
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(ConsoleUI.class).toInstance(init.ui);
-        bind(InitFlags.class).toInstance(init.flags);
+        bind(File.class).annotatedWith(SitePath.class).toInstance(getSitePath());
+        bind(Browser.class);
       }
     });
-    return createDbInjector(SINGLE_USER).createChildInjector(modules);
+    modules.add(new GerritServerConfigModule());
+    Guice.createInjector(modules).injectMembers(this);
+    start(run);
   }
 
-  private static void recursiveDelete(File path) {
-    File[] entries = path.listFiles();
-    if (entries != null) {
-      for (File e : entries) {
-        recursiveDelete(e);
+  @Override
+  protected List<String> getInstallPlugins() {
+    return installPlugins;
+  }
+
+  @Override
+  protected ConsoleUI getConsoleUI() {
+    return ConsoleUI.getInstance(batchMode);
+  }
+
+  @Override
+  protected boolean getAutoStart() {
+    return !noAutoStart;
+  }
+
+  @Override
+  protected boolean skipPlugins() {
+    return skipPlugins;
+  }
+
+  void start(SiteRun run) throws Exception {
+    if (run.flags.autoStart) {
+      if (HostPlatform.isWin32()) {
+        System.err.println("Automatic startup not supported on Win32.");
+
+      } else {
+        startDaemon(run);
+        if (!run.ui.isBatch()) {
+          browser.open(PageLinks.ADMIN_PROJECTS);
+        }
       }
     }
-    if (!path.delete() && path.exists()) {
-      System.err.println("warn: Cannot remove " + path);
+  }
+
+  void startDaemon(SiteRun run) {
+    final String[] argv = {run.site.gerrit_sh.getAbsolutePath(), "start"};
+    final Process proc;
+    try {
+      System.err.println("Executing " + argv[0] + " " + argv[1]);
+      proc = Runtime.getRuntime().exec(argv);
+    } catch (IOException e) {
+      System.err.println("error: cannot start Gerrit: " + e.getMessage());
+      return;
     }
+
+    try {
+      proc.getOutputStream().close();
+    } catch (IOException e) {
+    }
+
+    IoUtil.copyWithThread(proc.getInputStream(), System.err);
+    IoUtil.copyWithThread(proc.getErrorStream(), System.err);
+
+    for (;;) {
+      try {
+        final int rc = proc.waitFor();
+        if (rc != 0) {
+          System.err.println("error: cannot start Gerrit: exit status " + rc);
+        }
+        break;
+      } catch (InterruptedException e) {
+        // retry
+      }
+    }
+  }
+
+  private void verifyInstallPluginList(ConsoleUI ui, List<PluginData> plugins) {
+    if (nullOrEmpty(installPlugins) || nullOrEmpty(plugins)) {
+      return;
+    }
+    ArrayList<String> copy = Lists.newArrayList(installPlugins);
+    List<String> pluginNames = Lists.transform(plugins, new Function<PluginData, String>() {
+      @Override
+      public String apply(PluginData input) {
+        return input.name;
+      }
+    });
+    copy.removeAll(pluginNames);
+    if (!copy.isEmpty()) {
+      ui.message("Cannot find plugin(s): %s\n", Joiner.on(", ").join(copy));
+      listPlugins = true;
+    }
+  }
+
+  private static boolean nullOrEmpty(List<?> list) {
+    return list == null || list.isEmpty();
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
new file mode 100644
index 0000000..e39dd3b
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -0,0 +1,235 @@
+// 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.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+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.cache.CacheRemovalListener;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.ChangeBatchIndexer;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.IndexModule.IndexType;
+import com.google.gerrit.server.index.NoIndexModule;
+import com.google.gerrit.server.patch.PatchListCacheImpl;
+import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.schema.DataSourceType;
+import com.google.gerrit.solr.SolrIndexModule;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.util.io.NullOutputStream;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class Reindex extends SiteProgram {
+  private static final Logger log = LoggerFactory.getLogger(Reindex.class);
+
+  @Option(name = "--threads", usage = "Number of threads to use for indexing")
+  private int threads = Runtime.getRuntime().availableProcessors();
+
+  @Option(name = "--schema-version",
+      usage = "Schema version to reindex; default is most recent version")
+  private Integer version;
+
+  @Option(name = "--output", usage = "Prefix for output; path for local disk index, or prefix for remote index")
+  private String outputBase;
+
+  @Option(name = "--verbose", usage = "Output debug information for each change")
+  private boolean verbose;
+
+  @Option(name = "--dry-run", usage = "Dry run: don't write anything to index")
+  private boolean dryRun;
+
+  private Injector dbInjector;
+  private Injector sysInjector;
+  private ChangeIndex index;
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+    dbInjector = createDbInjector(MULTI_USER);
+    if (IndexModule.getIndexType(dbInjector) == IndexType.SQL) {
+      throw die("index.type must be configured (or not SQL)");
+    }
+    limitThreads();
+    if (version == null) {
+      version = ChangeSchemas.getLatest().getVersion();
+    }
+    LifecycleManager dbManager = new LifecycleManager();
+    dbManager.add(dbInjector);
+    dbManager.start();
+
+    sysInjector = createSysInjector();
+    LifecycleManager sysManager = new LifecycleManager();
+    sysManager.add(sysInjector);
+    sysManager.start();
+
+    index = sysInjector.getInstance(IndexCollection.class).getSearchIndex();
+    index.markReady(false);
+    index.deleteAll();
+    int result = indexAll();
+    index.markReady(true);
+
+    sysManager.stop();
+    dbManager.stop();
+    return result;
+  }
+
+  private void limitThreads() {
+    Config cfg =
+        dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    boolean usePool = cfg.getBoolean("database", "connectionpool",
+        dbInjector.getInstance(DataSourceType.class).usePool());
+    int poolLimit = cfg.getInt("database", "poollimit",
+        DataSourceProvider.DEFAULT_POOL_LIMIT);
+    if (usePool && threads > poolLimit) {
+      log.warn("Limiting reindexing to " + poolLimit
+          + " threads due to database.poolLimit");
+      threads = poolLimit;
+    }
+  }
+
+  private Injector createSysInjector() {
+    List<Module> modules = Lists.newArrayList();
+    modules.add(PatchListCacheImpl.module());
+    AbstractModule changeIndexModule;
+    switch (IndexModule.getIndexType(dbInjector)) {
+      case LUCENE:
+        changeIndexModule = new LuceneIndexModule(version, threads, outputBase);
+        break;
+      case SOLR:
+        changeIndexModule = new SolrIndexModule(false, threads, outputBase);
+        break;
+      default:
+        changeIndexModule = new NoIndexModule();
+    }
+    modules.add(changeIndexModule);
+    modules.add(new ReviewDbModule());
+    modules.add(new AbstractModule() {
+      @SuppressWarnings("rawtypes")
+      @Override
+      protected void configure() {
+        // Plugins are not loaded and we're just running through each change
+        // once, so don't worry about cache removal.
+        bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
+            .toInstance(DynamicSet.<CacheRemovalListener> emptySet());
+        install(new DefaultCacheFactory.Module());
+      }
+    });
+    return dbInjector.createChildInjector(modules);
+  }
+
+  private class ReviewDbModule extends LifecycleModule {
+    @Override
+    protected void configure() {
+      final SchemaFactory<ReviewDb> schema = dbInjector.getInstance(
+          Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
+      final List<ReviewDb> dbs = Collections.synchronizedList(
+          Lists.<ReviewDb> newArrayListWithCapacity(threads + 1));
+      final ThreadLocal<ReviewDb> localDb = new ThreadLocal<ReviewDb>();
+
+      bind(ReviewDb.class).toProvider(new Provider<ReviewDb>() {
+        @Override
+        public ReviewDb get() {
+          ReviewDb db = localDb.get();
+          if (db == null) {
+            try {
+              db = schema.open();
+              dbs.add(db);
+              localDb.set(db);
+            } catch (OrmException e) {
+              throw new ProvisionException("unable to open ReviewDb", e);
+            }
+          }
+          return db;
+        }
+      });
+      listener().toInstance(new LifecycleListener() {
+        @Override
+        public void start() {
+          // Do nothing.
+        }
+
+        @Override
+        public void stop() {
+          for (ReviewDb db : dbs) {
+            db.close();
+          }
+        }
+      });
+    }
+  }
+
+  private int indexAll() throws Exception {
+    ReviewDb db = sysInjector.getInstance(ReviewDb.class);
+    ProgressMonitor pm = new TextProgressMonitor();
+    pm.start(1);
+    pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
+    Set<Project.NameKey> projects = Sets.newTreeSet();
+    int changeCount = 0;
+    try {
+      for (Change change : db.changes().all()) {
+        changeCount++;
+        if (projects.add(change.getProject())) {
+          pm.update(1);
+        }
+      }
+    } finally {
+      db.close();
+    }
+    pm.endTask();
+
+    ChangeBatchIndexer batchIndexer =
+        sysInjector.getInstance(ChangeBatchIndexer.class);
+    ChangeBatchIndexer.Result result = batchIndexer.indexAll(
+      index, projects, projects.size(), changeCount, System.err,
+      verbose ? System.out : NullOutputStream.INSTANCE);
+    int n = result.doneCount() + result.failedCount();
+    double t = result.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+    System.out.format("Reindexed %d changes in %.01fs (%.01f/s)\n", n, t, n/t);
+    return result.success() ? 0 : 1;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
deleted file mode 100644
index 67f5c91..0000000
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
+++ /dev/null
@@ -1,28 +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.pgm;
-
-/** Policy for auto upgrading schema on server startup */
-public enum SchemaUpgradePolicy {
-
-  /** Perform schema migration if necessary and prune unused objects */
-  AUTO,
-
-  /** Like AUTO but don't prune unused objects */
-  AUTO_NO_PRUNE,
-
-  /** No automatic schema upgrade */
-  OFF
-}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java
index f54b3c5..4f35f1c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java
@@ -50,7 +50,7 @@
     Module(@GerritServerConfig final Config cfg) {
       URI[] urls = JettyServer.listenURLs(cfg);
       boolean reverseProxy = JettyServer.isReverseProxied(urls);
-      this.loggingEnabled = cfg.getBoolean("httpd", "requestlog", !reverseProxy);
+      this.loggingEnabled = cfg.getBoolean("httpd", "requestLog", !reverseProxy);
     }
 
     @Override
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
new file mode 100644
index 0000000..e086e6a
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -0,0 +1,83 @@
+// 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.pgm.http.jetty;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Strings;
+import com.google.gwtexpui.server.CacheHeaders;
+
+import org.eclipse.jetty.http.HttpHeaders;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.AbstractHttpConnection;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class HiddenErrorHandler extends ErrorHandler {
+  private static final Logger log = LoggerFactory.getLogger(HiddenErrorHandler.class);
+
+  public void handle(String target, Request baseRequest,
+      HttpServletRequest req, HttpServletResponse res) throws IOException {
+    AbstractHttpConnection conn = AbstractHttpConnection.getCurrentConnection();
+    conn.getRequest().setHandled(true);
+    try {
+      log(req);
+    } finally {
+      reply(conn, res);
+    }
+  }
+
+  private void reply(AbstractHttpConnection conn, HttpServletResponse res)
+      throws IOException {
+    byte[] msg = message(conn);
+    res.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain; charset=ISO-8859-1");
+    res.setContentLength(msg.length);
+    try {
+      CacheHeaders.setNotCacheable(res);
+    } finally {
+      ServletOutputStream out = res.getOutputStream();
+      try {
+        out.write(msg);
+      } finally {
+        out.close();
+      }
+    }
+  }
+
+  private static byte[] message(AbstractHttpConnection conn) {
+    String msg = conn.getResponse().getReason();
+    if (msg == null)
+      msg = HttpStatus.getMessage(conn.getResponse().getStatus());
+    return msg.getBytes(Charsets.ISO_8859_1);
+  }
+
+  private static void log(HttpServletRequest req) {
+    Throwable err = (Throwable)req.getAttribute("javax.servlet.error.exception");
+    if (err != null) {
+      String uri = req.getRequestURI();
+      if (!Strings.isNullOrEmpty(req.getQueryString())) {
+        uri += "?" + req.getQueryString();
+      }
+      log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
index 6f439d2..6aef509 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.TimeUtil;
 
 import org.apache.log4j.Appender;
 import org.apache.log4j.AsyncAppender;
@@ -95,7 +96,7 @@
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
         log, // logger
-        System.currentTimeMillis(), // when
+        TimeUtil.nowMs(), // when
         Level.INFO, // level
         "", // message text
         "HTTPD", // thread name
@@ -111,7 +112,7 @@
       uri = uri + "?" + qs;
     }
 
-    if (user instanceof IdentifiedUser) {
+    if (user != null && user.isIdentifiedUser()) {
       IdentifiedUser who = (IdentifiedUser) user;
       if (who.getUserName() != null && !who.getUserName().isEmpty()) {
         event.setProperty(P_USER, who.getUserName());
@@ -162,7 +163,7 @@
       dateFormat = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z");
       dateFormat.setTimeZone(tz);
 
-      lastTimeMillis = System.currentTimeMillis();
+      lastTimeMillis = TimeUtil.nowMs();
       lastTimeString = dateFormat.format(new Date(lastTimeMillis));
     }
 
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 3a7a874..e1d1281b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -17,12 +17,15 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.base.Objects;
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
@@ -49,12 +52,16 @@
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.util.thread.ThreadPool;
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InterruptedIOException;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -64,14 +71,24 @@
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Properties;
 import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
 import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
 
 @Singleton
 public class JettyServer {
+  private static final Logger log = LoggerFactory.getLogger(JettyServer.class);
+
   static class Lifecycle implements LifecycleListener {
     private final JettyServer server;
 
@@ -119,7 +136,7 @@
     httpd.setThreadPool(threadPool(cfg));
 
     Handler app = makeContext(env, cfg);
-    if (cfg.getBoolean("httpd", "requestlog", !reverseProxy)) {
+    if (cfg.getBoolean("httpd", "requestLog", !reverseProxy)) {
       RequestLogHandler handler = new RequestLogHandler();
       handler.setRequestLog(new HttpLog(site, cfg));
       handler.setHandler(app);
@@ -177,6 +194,12 @@
 
         if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
           ssl.setNeedClientAuth(true);
+
+          File crl = getFile(cfg, "sslcrl", "etc/crl.pem");
+          if (crl.exists()) {
+            ssl.setCrlPath(crl.getAbsolutePath());
+            ssl.setValidatePeerCerts(true);
+          }
         }
 
         defaultPort = 443;
@@ -296,7 +319,7 @@
 
     final List<ContextHandler> all = new ArrayList<ContextHandler>();
     for (String path : paths) {
-      all.add(makeContext(path, env));
+      all.add(makeContext(path, env, cfg));
     }
 
     if (all.size() == 1) {
@@ -316,13 +339,14 @@
   }
 
   private ContextHandler makeContext(final String contextPath,
-      final JettyEnv env) throws MalformedURLException, IOException {
+      final JettyEnv env, final Config cfg) throws MalformedURLException, IOException {
     final ServletContextHandler app = new ServletContextHandler();
 
     // This enables the use of sessions in Jetty, feature available
     // for Gerrit plug-ins to enable user-level sessions.
     //
     app.setSessionHandler(new SessionHandler());
+    app.setErrorHandler(new HiddenErrorHandler());
 
     // This is the path we are accessed by clients within our domain.
     //
@@ -332,7 +356,28 @@
     // need to unpack them into yet another temporary directory prior to
     // serving to clients.
     //
-    app.setBaseResource(getBaseResource());
+    app.setBaseResource(getBaseResource(app));
+
+    // HTTP front-end filter to be used as surrogate of Apache HTTP
+    // reverse-proxy filtering.
+    // It is meant to be used as simpler tiny deployment of custom-made
+    // security enforcement (Security tokens, IP-based security filtering, others)
+    String filterClassName = cfg.getString("httpd", null, "filterClass");
+    if (filterClassName != null) {
+      try {
+        @SuppressWarnings("unchecked")
+        Class<? extends Filter> filterClass =
+            (Class<? extends Filter>) Class.forName(filterClassName);
+        Filter filter = env.webInjector.getInstance(filterClass);
+        app.addFilter(new FilterHolder(filter), "/*",
+            EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
+      } catch (Throwable e) {
+        String errorMessage =
+            "Unable to instantiate front-end HTTP Filter " + filterClassName;
+        log.error(errorMessage, e);
+        throw new IllegalArgumentException(errorMessage, e);
+      }
+    }
 
     // Perform the same binding as our web.xml would do, but instead
     // of using the listener to create the injector pass the one we
@@ -365,13 +410,14 @@
     return app;
   }
 
-  private Resource getBaseResource() throws IOException {
+  private Resource getBaseResource(ServletContextHandler app)
+      throws IOException {
     if (baseResource == null) {
       try {
-        baseResource = unpackWar();
+        baseResource = unpackWar(GerritLauncher.getDistributionArchive());
       } catch (FileNotFoundException err) {
         if (err.getMessage() == GerritLauncher.NOT_ARCHIVED) {
-          baseResource = useDeveloperBuild();
+          baseResource = useDeveloperBuild(app);
         } else {
           throw err;
         }
@@ -380,9 +426,13 @@
     return baseResource;
   }
 
-  private Resource unpackWar() throws IOException {
-    final File srcwar = GerritLauncher.getDistributionArchive();
+  private static Resource unpackWar(File srcwar) throws IOException {
+    File dstwar = makeWarTempDir();
+    unpack(srcwar, dstwar);
+    return Resource.newResource(dstwar.toURI());
+  }
 
+  private static File makeWarTempDir() throws IOException {
     // Obtain our local temporary directory, but it comes back as a file
     // so we have to switch it to be a directory post creation.
     //
@@ -395,11 +445,13 @@
     // a security feature. Try to resolve out any symlinks in the path.
     //
     try {
-      dstwar = dstwar.getCanonicalFile();
+      return dstwar.getCanonicalFile();
     } catch (IOException e) {
-      dstwar = dstwar.getAbsoluteFile();
+      return dstwar.getAbsoluteFile();
     }
+  }
 
+  private static void unpack(File srcwar, File dstwar) throws IOException {
     final ZipFile zf = new ZipFile(srcwar);
     try {
       final Enumeration<? extends ZipEntry> e = zf.entries();
@@ -436,11 +488,9 @@
     } finally {
       zf.close();
     }
-
-    return Resource.newResource(dstwar.toURI());
   }
 
-  private void mkdir(final File dir) throws IOException {
+  private static void mkdir(File dir) throws IOException {
     if (!dir.isDirectory()) {
       mkdir(dir.getParentFile());
       if (!dir.mkdir())
@@ -449,7 +499,8 @@
     }
   }
 
-  private Resource useDeveloperBuild() throws IOException {
+  private Resource useDeveloperBuild(ServletContextHandler app)
+      throws IOException {
     // Find ourselves in the CLASSPATH. We should be a loose class file.
     //
     URL u = getClass().getResource(getClass().getSimpleName() + ".class");
@@ -474,15 +525,119 @@
       dir = dir.getParentFile();
     }
 
-    // We should be in a Maven style output, that is $jar/target/classes.
-    //
     if (!dir.getName().equals("classes")) {
       throw new FileNotFoundException("Cannot find web root from " + u);
     }
     dir = dir.getParentFile(); // pop classes
-    if (!dir.getName().equals("target")) {
+
+    if ("buck-out".equals(dir.getName())) {
+      final File dstwar = makeWarTempDir();
+      String pkg = "gerrit-gwtui";
+      String target = targetForBrowser(System.getProperty("gerrit.browser"));
+      final File gen = new File(dir, "gen");
+      String out = new File(new File(gen, pkg), target).getAbsolutePath();
+      final File zip = new File(out + ".zip");
+      final File root = dir.getParentFile();
+      final String name = "//" + pkg + ":" + target;
+
+      File ui = new File(dstwar, "gerrit_ui");
+      File p = new File(ui, "permutations");
+      mkdir(ui);
+      p.createNewFile();
+      p.deleteOnExit();
+
+      app.addFilter(new FilterHolder(new Filter() {
+        private long last;
+
+        @Override
+        public void doFilter(ServletRequest request, ServletResponse res,
+            FilterChain chain) throws IOException, ServletException {
+          HttpServletRequest req = (HttpServletRequest) request;
+          build(root, gen, name);
+          if (last != zip.lastModified()) {
+            last = zip.lastModified();
+            unpack(zip, dstwar);
+          }
+          chain.doFilter(req, res);
+        }
+
+        @Override
+        public void init(FilterConfig config) {
+        }
+        @Override
+        public void destroy() {
+        }
+      }), "/", EnumSet.of(DispatcherType.REQUEST));
+      return Resource.newResource(dstwar.toURI());
+    } else if ("target".equals(dir.getName())) {
+      return useMavenDeveloperBuild(dir);
+    } else {
       throw new FileNotFoundException("Cannot find web root from " + u);
     }
+  }
+
+  private static String targetForBrowser(String browser) {
+    if (browser == null || browser.isEmpty()) {
+      return "ui_dbg";
+    } else if (browser.startsWith("ui_")) {
+      return browser;
+    } else {
+      return "ui_" + browser;
+    }
+  }
+
+  private static void build(File root, File gen, String target)
+      throws IOException {
+    log.info("buck build " + target);
+    Properties properties = loadBuckProperties(gen);
+    String buck = Objects.firstNonNull(properties.getProperty("buck"), "buck");
+    ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
+        .directory(root)
+        .redirectErrorStream(true);
+    if (properties.containsKey("PATH")) {
+      proc.environment().put("PATH", properties.getProperty("PATH"));
+    }
+    long start = TimeUtil.nowMs();
+    Process rebuild = proc.start();
+    byte[] out;
+    InputStream in = rebuild.getInputStream();
+    try {
+      out = ByteStreams.toByteArray(in);
+    } finally {
+      rebuild.getOutputStream().close();
+      in.close();
+    }
+
+    int status;
+    try {
+      status = rebuild.waitFor();
+    } catch (InterruptedException e) {
+      throw new InterruptedIOException("interrupted waiting for " + buck);
+    }
+    if (status != 0) {
+      System.err.write(out);
+      System.err.println();
+      System.exit(status);
+    }
+
+    long time = TimeUtil.nowMs() - start;
+    log.info(String.format("UPDATED    %s in %.3fs", target, time / 1000.0));
+  }
+
+  private static Properties loadBuckProperties(File gen)
+      throws FileNotFoundException, IOException {
+    Properties properties = new Properties();
+    InputStream in = new FileInputStream(
+        new File(new File(gen, "tools"), "buck.properties"));
+    try {
+      properties.load(in);
+    } finally {
+      in.close();
+    }
+    return properties;
+  }
+
+  private Resource useMavenDeveloperBuild(File dir) throws IOException {
     dir = dir.getParentFile(); // pop target
     dir = dir.getParentFile(); // pop the module we are in
 
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 8d320d4..7730fa5 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
@@ -227,7 +227,7 @@
       String userName = "";
 
       CurrentUser who = userProvider.get();
-      if (who instanceof IdentifiedUser) {
+      if (who.isIdentifiedUser()) {
         String name = ((IdentifiedUser) who).getUserName();
         if (name != null && !name.isEmpty()) {
           userName = " (" + name + ")";
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
index 8e3948e..96ab103 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
@@ -39,12 +39,15 @@
   }
 
   public void open(final String link) throws Exception {
-    String url = cfg.getString("httpd", null, "listenUrl");
+    String url = cfg.getString("gerrit", null, "canonicalWebUrl");
     if (url == null) {
-      return;
-    }
-    if (url.startsWith("proxy-")) {
-      url = url.substring("proxy-".length());
+      url = cfg.getString("httpd", null, "listenUrl");
+      if (url == null) {
+        return;
+      }
+      if (url.startsWith("proxy-")) {
+        url = url.substring("proxy-".length());
+      }
     }
 
     final URI uri;
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 32f8c2e..55419c2 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
@@ -36,6 +36,8 @@
     bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("mysql")).to(MySqlInitializer.class);
     bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("oracle")).to(OracleInitializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("postgresql")).to(PostgreSQLInitializer.class);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
index fbd543d..aa584e8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
@@ -57,7 +57,7 @@
     try {
       myWar = GerritLauncher.getDistributionArchive();
     } catch (FileNotFoundException e) {
-      System.err.println("warn: Cannot find gerrit.war");
+      System.err.println("warn: Cannot find distribution archive (e.g. gerrit.war)");
       myWar = null;
     }
 
@@ -75,7 +75,7 @@
       if (siteWar.exists()) {
         copy = ui.yesno(true, "Upgrade %s", siteWar.getPath());
       } else {
-        copy = ui.yesno(true, "Copy gerrit.war to %s", siteWar.getPath());
+        copy = ui.yesno(true, "Copy %s to %s", myWar.getName(), siteWar.getPath());
         if (copy) {
           container.unset("war");
         } else {
@@ -84,7 +84,7 @@
       }
       if (copy) {
         if (!ui.isBatch()) {
-          System.err.format("Copying gerrit.war to %s", siteWar.getPath());
+          System.err.format("Copying %s to %s", myWar.getName(), siteWar.getPath());
           System.err.println();
         }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index aa413d6..2120a73 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -78,7 +78,9 @@
             Names.named(dbType.toLowerCase())));
 
     if (dci instanceof MySqlInitializer) {
-        libraries.mysqlDriver.downloadRequired();
+      libraries.mysqlDriver.downloadRequired();
+    } else if (dci instanceof OracleInitializer) {
+      libraries.oracleDriver.downloadRequired();
     }
 
     dci.initConfig(database);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
index 5d71b48..267b41a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
@@ -23,6 +23,7 @@
 import org.eclipse.jgit.util.FS;
 
 import java.io.IOException;
+import java.util.List;
 
 /** Global variables used by the 'init' command. */
 @Singleton
@@ -33,11 +34,19 @@
   /** Run the daemon (and open the web UI in a browser) after initialization. */
   public boolean autoStart;
 
+  /** Skip plugins */
+  public boolean skipPlugins;
+
   public final FileBasedConfig cfg;
   public final FileBasedConfig sec;
+  public final List<String> installPlugins;
+
 
   @Inject
-  InitFlags(final SitePaths site) throws IOException, ConfigInvalidException {
+  InitFlags(final SitePaths site,
+      final @InstallPlugins List<String> installPlugins) throws IOException,
+      ConfigInvalidException {
+    this.installPlugins = installPlugins;
     cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
     sec = new FileBasedConfig(site.secure_config, FS.DETECTED);
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index 8b3d87e..94185eb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -23,6 +23,13 @@
 
 /** Injection configuration for the site initialization process. */
 public class InitModule extends FactoryModule {
+
+  private final boolean standalone;
+
+  public InitModule(boolean standalone) {
+    this.standalone = standalone;
+  }
+
   @Override
   protected void configure() {
     bind(SitePaths.class);
@@ -36,14 +43,20 @@
     step().to(UpgradeFrom2_0_x.class);
 
     step().to(InitGitManager.class);
-    step().to(InitDatabase.class);
+    if (standalone) {
+      step().to(InitDatabase.class);
+    }
     step().to(InitAuth.class);
     step().to(InitSendEmail.class);
-    step().to(InitContainer.class);
+    if (standalone) {
+      step().to(InitContainer.class);
+    }
     step().to(InitSshd.class);
     step().to(InitHttpd.class);
     step().to(InitCache.class);
-    step().to(InitPlugins.class);
+    if (standalone) {
+      step().to(InitPlugins.class);
+    }
   }
 
   protected LinkedBindingBuilder<InitStep> step() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index 7658701..6b4cedf 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.common.base.Objects;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugins.PluginLoader;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -24,6 +26,7 @@
 
 import java.io.File;
 import java.io.FileFilter;
+import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.ArrayList;
@@ -90,10 +93,10 @@
     }
   }
 
-  private Injector getPluginInjector(File jarFile) {
-    String jarFileName = jarFile.getName();
+  private Injector getPluginInjector(File jarFile) throws IOException {
     final String pluginName =
-        jarFileName.substring(0, jarFileName.lastIndexOf('.'));
+        Objects.firstNonNull(PluginLoader.getGerritPluginName(jarFile),
+            PluginLoader.nameOf(jarFile));
     return initInjector.createChildInjector(new AbstractModule() {
       @Override
       protected void configure() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index b82df64..9c8305a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
@@ -22,10 +23,10 @@
 import com.google.inject.Singleton;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
@@ -34,17 +35,75 @@
 
 @Singleton
 public class InitPlugins implements InitStep {
-  private final static String PLUGIN_DIR = "WEB-INF/plugins/";
-  private final static String JAR = ".jar";
+  private static final String PLUGIN_DIR = "WEB-INF/plugins/";
+  private static final String JAR = ".jar";
+
+  public static class PluginData {
+    public final String name;
+    public final String version;
+    public final File pluginFile;
+
+    private PluginData(String name, String version, File pluginFile) {
+      this.name = name;
+      this.version = version;
+      this.pluginFile = pluginFile;
+    }
+  }
+
+  public static List<PluginData> listPlugins(SitePaths site) throws IOException {
+    return listPlugins(site, false);
+  }
+
+  public static List<PluginData> listPluginsAndRemoveTempFiles(SitePaths site) throws IOException {
+    return listPlugins(site, true);
+  }
+
+  private static List<PluginData> listPlugins(SitePaths site, boolean deleteTempPluginFile) throws IOException {
+    final File myWar = GerritLauncher.getDistributionArchive();
+    final List<PluginData> result = Lists.newArrayList();
+    try {
+      final ZipFile zf = new ZipFile(myWar);
+      try {
+        final Enumeration<? extends ZipEntry> e = zf.entries();
+        while (e.hasMoreElements()) {
+          final ZipEntry ze = e.nextElement();
+          if (ze.isDirectory()) {
+            continue;
+          }
+
+          if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
+            final String pluginJarName = new File(ze.getName()).getName();
+            final String pluginName = pluginJarName.substring(0,  pluginJarName.length() - JAR.length());
+            final InputStream in = zf.getInputStream(ze);
+            final File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
+            final String pluginVersion = getVersion(tmpPlugin);
+            if (deleteTempPluginFile) {
+              tmpPlugin.delete();
+            }
+
+            result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
+          }
+        }
+      } finally {
+        zf.close();
+      }
+    } catch (IOException e) {
+      throw new IOException("Failure during plugin installation", e);
+    }
+    return result;
+  }
 
   private final ConsoleUI ui;
   private final SitePaths site;
-  private InitPluginStepsLoader pluginLoader;
+  private final InitFlags initFlags;
+  private final InitPluginStepsLoader pluginLoader;
 
   @Inject
-  InitPlugins(final ConsoleUI ui, final SitePaths site, InitPluginStepsLoader pluginLoader) {
+  InitPlugins(final ConsoleUI ui, final SitePaths site,
+      InitFlags initFlags, InitPluginStepsLoader pluginLoader) {
     this.ui = ui;
     this.site = site;
+    this.initFlags = initFlags;
     this.pluginLoader = pluginLoader;
   }
 
@@ -57,79 +116,44 @@
   }
 
   private void installPlugins() throws IOException {
-    final File myWar;
-    try {
-      myWar = GerritLauncher.getDistributionArchive();
-    } catch (FileNotFoundException e) {
-      System.err.println("warn: Cannot find gerrit.war");
-      return;
-    }
-
-    boolean foundPlugin = false;
-    try {
-      final ZipFile zf = new ZipFile(myWar);
+    List<PluginData> plugins = listPlugins(site);
+    for (PluginData plugin : plugins) {
+      String pluginName = plugin.name;
       try {
-        final Enumeration<? extends ZipEntry> e = zf.entries();
-        while (e.hasMoreElements()) {
-          final ZipEntry ze = e.nextElement();
-          if (ze.isDirectory()) {
+        final File tmpPlugin = plugin.pluginFile;
+
+        if (!(initFlags.installPlugins.contains(pluginName) || ui.yesno(false,
+            "Install plugin %s version %s", pluginName, plugin.version))) {
+          tmpPlugin.delete();
+          continue;
+        }
+
+        final File p = new File(site.plugins_dir, plugin.name + ".jar");
+        if (p.exists()) {
+          final String installedPluginVersion = getVersion(p);
+          if (!ui.yesno(false,
+              "version %s is already installed, overwrite it",
+              installedPluginVersion)) {
+            tmpPlugin.delete();
             continue;
           }
-
-          if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
-            if (!foundPlugin) {
-              if (!ui.yesno(false, "Prompt to install core plugins")) {
-                return;
-              }
-              foundPlugin = true;
-            }
-
-            final String pluginJarName = new File(ze.getName()).getName();
-            final String pluginName = pluginJarName.substring(0,  pluginJarName.length() - JAR.length());
-
-            final InputStream in = zf.getInputStream(ze);
-            try {
-              final File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
-              final String pluginVersion = getVersion(tmpPlugin);
-
-              if (!ui.yesno(false, "Install plugin %s version %s", pluginName,
-                  pluginVersion)) {
-                tmpPlugin.delete();
-                continue;
-              }
-
-              final File plugin = new File(site.plugins_dir, pluginJarName);
-              if (plugin.exists()) {
-                final String installedPluginVersion = getVersion(plugin);
-                if (!ui.yesno(false,
-                    "version %s is already installed, overwrite it",
-                    installedPluginVersion)) {
-                  tmpPlugin.delete();
-                  continue;
-                }
-                if (!plugin.delete()) {
-                  throw new IOException("Failed to delete plugin " + pluginName
-                      + ": " + plugin.getAbsolutePath());
-                }
-              }
-              if (!tmpPlugin.renameTo(plugin)) {
-                throw new IOException("Failed to install plugin " + pluginName
-                    + ": " + tmpPlugin.getAbsolutePath() + " -> "
-                    + plugin.getAbsolutePath());
-              }
-            } finally {
-              in.close();
-            }
+          if (!p.delete()) {
+            throw new IOException("Failed to delete plugin " + pluginName
+                + ": " + p.getAbsolutePath());
           }
         }
+        if (!tmpPlugin.renameTo(p)) {
+          throw new IOException("Failed to install plugin " + pluginName
+              + ": " + tmpPlugin.getAbsolutePath() + " -> "
+              + p.getAbsolutePath());
+        }
       } finally {
-        zf.close();
+        if (plugin.pluginFile.exists()) {
+          plugin.pluginFile.delete();
+        }
       }
-    } catch (IOException e) {
-      throw new IOException("Failure during plugin installation", e);
     }
-
-    if (!foundPlugin) {
+    if (plugins.isEmpty()) {
       ui.message("No plugins found.");
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
new file mode 100644
index 0000000..73db6f5
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
@@ -0,0 +1,25 @@
+// 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.pgm.init;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@BindingAnnotation
+@Retention(RetentionPolicy.RUNTIME)
+public @interface InstallPlugins {
+}
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 b1fa0c3..a03144b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -39,6 +39,7 @@
 
   /* final */LibraryDownloader bouncyCastle;
   /* final */LibraryDownloader mysqlDriver;
+  /* final */LibraryDownloader oracleDriver;
 
   @Inject
   Libraries(final Provider<LibraryDownloader> downloadProvider) {
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 bf358f4..7caf49a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.base.Strings;
+import com.google.common.io.Files;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.pgm.util.Die;
 import com.google.gerrit.pgm.util.IoUtil;
@@ -35,6 +36,7 @@
 import java.net.HttpURLConnection;
 import java.net.Proxy;
 import java.net.ProxySelector;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -50,6 +52,7 @@
   private String sha1;
   private String remove;
   private File dst;
+  private boolean download; // download or copy
 
   @Inject
   LibraryDownloader(ConsoleUI ui, SitePaths site) {
@@ -63,6 +66,7 @@
 
   void setJarUrl(final String url) {
     this.jarUrl = url;
+    download = jarUrl.startsWith("http");
   }
 
   void setSHA1(final String sha1) {
@@ -117,7 +121,8 @@
         msg.append("  If available, Gerrit can take advantage of features\n");
         msg.append("  in the library, but will also function without it.\n");
       }
-      msg.append("Download and install it now");
+      msg.append(String.format(
+          "%s and install it now", download ? "Download" : "Copy"));
       return ui.yesno(true, msg.toString(), name);
     }
   }
@@ -129,7 +134,11 @@
 
     try {
       removeStaleVersions();
-      doGetByHttp();
+      if (download) {
+        doGetByHttp();
+      } else {
+        doGetByLocalCopy();
+      }
       verifyFileChecksum();
     } catch (IOException err) {
       dst.delete();
@@ -186,6 +195,28 @@
     }
   }
 
+  private void doGetByLocalCopy() throws IOException {
+    System.err.print("Copying " + jarUrl + " ...");
+    File f = url2file(jarUrl);
+    if (!f.exists()) {
+      StringBuilder msg = new StringBuilder()
+          .append("\n")
+          .append("Can not find the %s at this location: %s\n")
+          .append("Please provide alternative URL");
+      f = url2file(ui.readString(null, msg.toString(), name, jarUrl));
+    }
+    Files.copy(f, dst);
+  }
+
+  private static File url2file(final String urlString) throws IOException {
+    final URL url = new URL(urlString);
+    try {
+      return new File(url.toURI());
+    } catch (URISyntaxException e) {
+      return new File(url.getPath());
+    }
+  }
+
   private void doGetByHttp() throws IOException {
     System.err.print("Downloading " + jarUrl + " ...");
     System.err.flush();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java
new file mode 100644
index 0000000..180beb0
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java
@@ -0,0 +1,31 @@
+// 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.pgm.init;
+
+import static com.google.gerrit.pgm.init.InitUtil.username;
+
+
+public class OracleInitializer implements DatabaseConfigInitializer {
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    final String defPort = "1521";
+    databaseSection.string("Server hostname", "hostname", "localhost");
+    databaseSection.string("Server port", "port", defPort, false);
+    databaseSection.string("Instance name", "instance", "xe");
+    databaseSection.string("Database username", "username", username());
+    databaseSection.password("username", "password");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
index 387b93a..26a422e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.SitePaths;
@@ -25,8 +26,6 @@
 import java.util.Arrays;
 import java.util.Set;
 
-import javax.annotation.Nullable;
-
 /** Helper to edit a section of the configuration files. */
 public class Section {
   public interface Factory {
@@ -96,7 +95,7 @@
       final boolean nullIfDefault) {
     final String ov = get(name);
     String nv = ui.readString(ov != null ? ov : dv, "%s", title);
-    if (nullIfDefault && nv == dv) {
+    if (nullIfDefault && nv.equals(dv)) {
       nv = null;
     }
     if (!eq(ov, nv)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index bf0af7f..71a4d86 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -22,7 +22,7 @@
 import static com.google.gerrit.pgm.init.InitUtil.saveSecure;
 import static com.google.gerrit.pgm.init.InitUtil.version;
 
-import com.google.gerrit.pgm.Init;
+import com.google.gerrit.pgm.BaseInit;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.mail.OutgoingEmail;
@@ -75,13 +75,17 @@
     mkdir(site.data_dir);
 
     for (InitStep step : steps) {
+      if (step instanceof InitPlugins
+          && flags.skipPlugins) {
+        continue;
+      }
       step.run();
     }
 
     savePublic(flags.cfg);
     saveSecure(flags.sec);
 
-    extract(site.gerrit_sh, Init.class, "gerrit.sh");
+    extract(site.gerrit_sh, BaseInit.class, "gerrit.sh");
     chmod(0755, site.gerrit_sh);
     chmod(0700, site.tmp_dir);
 
@@ -91,10 +95,10 @@
     extractMailExample("Comment.vm");
     extractMailExample("CommentFooter.vm");
     extractMailExample("CommitMessageEdited.vm");
+    extractMailExample("Footer.vm");
     extractMailExample("Merged.vm");
     extractMailExample("MergeFail.vm");
     extractMailExample("NewChange.vm");
-    extractMailExample("RebasedPatchSet.vm");
     extractMailExample("RegisterNewEmail.vm");
     extractMailExample("ReplacePatchSet.vm");
     extractMailExample("Restored.vm");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
index 833b4d8..af65170 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
@@ -16,15 +16,12 @@
 
 
 import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.Module;
+import com.google.gerrit.util.cli.OptionHandlers;
 
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
 
 import java.io.StringWriter;
-import java.util.Collections;
 
 /** Base class for command line invocations of Gerrit Code Review. */
 public abstract class AbstractProgram {
@@ -44,8 +41,7 @@
   }
 
   public final int main(final String[] argv) throws Exception {
-    final Injector empty = emptyInjector();
-    final CmdLineParser clp = new CmdLineParser(empty, this);
+    final CmdLineParser clp = new CmdLineParser(OptionHandlers.empty(), this);
     try {
       clp.parseArgument(argv);
     } catch (CmdLineException err) {
@@ -81,10 +77,6 @@
     }
   }
 
-  private static Injector emptyInjector() {
-    return Guice.createInjector(Collections.<Module> emptyList());
-  }
-
   /** Create a new exception to indicate we won't continue. */
   protected static Die die(String why) {
     return new Die(why);
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 fdc8fb3..126ef98 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
@@ -34,6 +34,7 @@
 import java.io.IOException;
 
 public class ErrorLogFile {
+  @SuppressWarnings("deprecation")
   private static final String LOG4J_CONFIGURATION = LogManager.DEFAULT_CONFIGURATION_KEY;
   static final String LOG_NAME = "error_log";
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index aae5b48..11968db 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -17,6 +17,8 @@
 import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
 
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
@@ -29,11 +31,15 @@
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
 import com.google.inject.name.Names;
 import com.google.inject.spi.Message;
 
@@ -41,6 +47,8 @@
 import org.kohsuke.args4j.Option;
 
 import java.io.File;
+import java.lang.annotation.Annotation;
+import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
@@ -51,6 +59,16 @@
   @Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data")
   private File sitePath = new File(".");
 
+  protected Provider<DataSource> dsProvider;
+
+  protected SiteProgram() {
+  }
+
+  protected SiteProgram(File sitePath, final Provider<DataSource> dsProvider) {
+    this.sitePath = sitePath;
+    this.dsProvider = dsProvider;
+  }
+
   /** @return the site path specified on the command line. */
   protected File getSitePath() {
     File path = sitePath.getAbsoluteFile();
@@ -85,17 +103,31 @@
       @Override
       protected void configure() {
         bind(DataSourceProvider.Context.class).toInstance(context);
-        bind(Key.get(DataSource.class, Names.named("ReviewDb")))
-          .toProvider(SiteLibraryBasedDataSourceProvider.class)
-          .in(SINGLETON);
-        listener().to(SiteLibraryBasedDataSourceProvider.class);
+        if (dsProvider != null) {
+          bind(Key.get(DataSource.class, Names.named("ReviewDb")))
+            .toProvider(dsProvider)
+            .in(SINGLETON);
+          if (LifecycleListener.class.isAssignableFrom(dsProvider.getClass())) {
+            listener().toInstance((LifecycleListener) dsProvider);
+          }
+        } else {
+          bind(Key.get(DataSource.class, Names.named("ReviewDb")))
+            .toProvider(SiteLibraryBasedDataSourceProvider.class)
+            .in(SINGLETON);
+          listener().to(SiteLibraryBasedDataSourceProvider.class);
+        }
       }
     });
     Module configModule = new GerritServerConfigModule();
     modules.add(configModule);
     Injector cfgInjector = Guice.createInjector(sitePathModule, configModule);
     Config cfg = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    String dbType = cfg.getString("database", null, "type");
+    String dbType;
+    if (dsProvider != null) {
+      dbType = getDbType(dsProvider);
+    } else {
+      dbType = cfg.getString("database", null, "type");
+    }
 
     final DataSourceType dst = Guice.createInjector(new DataSourceModule(), configModule,
             sitePathModule).getInstance(
@@ -144,6 +176,44 @@
     }
   }
 
+  private String getDbType(Provider<DataSource> dsProvider) {
+    String dbProductName;
+    try {
+      Connection conn = dsProvider.get().getConnection();
+      try {
+        dbProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
+      } finally {
+        conn.close();
+      }
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+
+    List<Module> modules = Lists.newArrayList();
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+      }
+    });
+    modules.add(new GerritServerConfigModule());
+    modules.add(new DataSourceModule());
+    Injector i = Guice.createInjector(modules);
+    List<Binding<DataSourceType>> dsTypeBindings =
+        i.findBindingsByType(new TypeLiteral<DataSourceType>() {});
+    for (Binding<DataSourceType> binding : dsTypeBindings) {
+      Annotation annotation = binding.getKey().getAnnotation();
+      if (annotation instanceof Named) {
+        if (((Named) annotation).value().toLowerCase().contains(dbProductName)) {
+          return ((Named) annotation).value();
+        }
+      }
+    }
+    throw new IllegalStateException(String.format(
+        "Cannot guess database type from the database product name '%s'",
+        dbProductName));
+  }
+
   @SuppressWarnings("deprecation")
   private static boolean isCannotCreatePoolException(Throwable why) {
     return why instanceof org.apache.commons.dbcp.SQLNestedException
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
index f1ecadd..6f80364 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 
+# Version should match lib/bouncycastle/BUCK
 [library "bouncyCastle"]
   name = Bouncy Castle Crypto v144
   url = http://www.bouncycastle.org/download/bcprov-jdk16-144.jar
@@ -24,3 +25,9 @@
   url = http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar
   sha1 = 7abbd19fc2e2d5b92c0895af8520f7fa30266be9
   remove = mysql-connector-java-.*[.]jar
+
+[library "oracleDriver"]
+  name = Oracle JDBC driver 11g Release 2 (11.2.0)
+  url = file:///u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
+  sha1 = 2f89cd9176772c3a6c261ce6a8e3d0d4425f5679
+  remove = ojdbc6.jar
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
index 86f0260..26c4382 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -37,6 +37,7 @@
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.Writer;
+import java.util.Collections;
 
 
 public class UpgradeFrom2_0_xTest extends InitTestCase {
@@ -68,7 +69,8 @@
     old.setString("sendemail", null, "smtpPass", "email.s3kr3t");
     old.save();
 
-    final InitFlags flags = new InitFlags(site);
+    final InitFlags flags =
+        new InitFlags(site, Collections.<String> emptyList());
     final ConsoleUI ui = createStrictMock(ConsoleUI.class);
     Section.Factory sections = new Section.Factory() {
       @Override
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
deleted file mode 100644
index 0bfc55d..0000000
--- a/gerrit-plugin-api/pom.xml
+++ /dev/null
@@ -1,158 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-plugin-api</artifactId>
-  <name>Gerrit Code Review - Plugin API</name>
-
-  <description>
-    API for tightly coupled plugins to compile against
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-sshd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-httpd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-pgm</artifactId>
-      <version>${project.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>org.eclipse.jetty</groupId>
-          <artifactId>jetty-servlet</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>servlet-api</artifactId>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-shade-plugin</artifactId>
-        <configuration>
-          <createSourcesJar>true</createSourcesJar>
-          <artifactSet>
-            <excludes>
-              <exclude>gwtjsonrpc:gwtjsonrpc</exclude>
-              <exclude>com.google.gerrit:gerrit-gwtexpui</exclude>
-              <exclude>com.google.gerrit:gerrit-prettify</exclude>
-              <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
-              <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
-              <exclude>com.google.gerrit:gerrit-util-ssl</exclude>
-
-              <exclude>com.googlecode.juniversalchardet:juniversalchardet</exclude>
-              <exclude>com.googlecode.prolog-cafe:PrologCafe</exclude>
-              <exclude>org.slf4j:slf4j-log4j12</exclude>
-              <exclude>log4j:log4j</exclude>
-
-              <exclude>commons-collections:commons-collections</exclude>
-              <exclude>commons-codec:commons-codec</exclude>
-              <exclude>commons-dbcp:commons-dbcp</exclude>
-              <exclude>commons-lang:commons-lang</exclude>
-              <exclude>commons-net:commons-net</exclude>
-              <exclude>commons-pool:commons-pool</exclude>
-
-              <exclude>asm:asm</exclude>
-              <exclude>eu.medsea.mimeutil:mime-util</exclude>
-              <exclude>org.antlr:antlr</exclude>
-              <exclude>org.antlr:antlr-runtime</exclude>
-              <exclude>org.apache.mina:mina-core</exclude>
-              <exclude>oro:oro</exclude>
-            </excludes>
-          </artifactSet>
-          <filters>
-            <filter>
-              <artifact>com.google.gerrit:gerrit-server</artifact>
-              <excludes>
-                <exclude>gerrit/**</exclude>
-              </excludes>
-            </filter>
-          </filters>
-        </configuration>
-        <executions>
-          <execution>
-            <phase>package</phase>
-            <goals>
-              <goal>shade</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-antrun-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>unpack-sources</id>
-            <phase>package</phase>
-            <configuration>
-              <tasks>
-                <unzip src="${project.build.directory}/${project.artifactId}-${project.version}-sources.jar" dest="${project.build.directory}/unpack_sources" />
-              </tasks>
-            </configuration>
-            <goals>
-              <goal>run</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-javadoc-plugin</artifactId>
-        <configuration>
-          <sourcepath>${project.build.directory}/unpack_sources</sourcepath>
-          <encoding>ISO-8859-1</encoding>
-          <quiet>true</quiet>
-          <detectOfflineLinks>false</detectOfflineLinks>
-        </configuration>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-            <phase>package</phase>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index f721be4..1b33510 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -18,13 +18,9 @@
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
+  <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-archetype</artifactId>
+  <version>2.8-SNAPSHOT</version>
   <name>Gerrit Code Review - Plugin Archetype</name>
 
   <properties>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
index 92099fa..cbf8a52 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -38,6 +38,7 @@
         <configuration>
           <archive>
             <manifestEntries>
+              <Gerrit-PluginName>${pluginName}</Gerrit-PluginName>
 #if ($Gerrit-Module.equalsIgnoreCase("Y"))
               <Gerrit-Module>${package}.Module</Gerrit-Module>
 #end
@@ -94,9 +95,9 @@
     <repository>
       <id>gerrit-api-repository</id>
 #if ($gerritApiVersion.endsWith("SNAPSHOT"))
-      <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+      <url>https://gerrit-api.storage.googleapis.com/snapshot/</url>
 #else
-      <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+      <url>https://gerrit-api.storage.googleapis.com/release/</url>
 #end
     </repository>
   </repositories>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 9ea0cd0..3c4dc99 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -18,13 +18,9 @@
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
+  <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
+  <version>2.8-SNAPSHOT</version>
   <name>Gerrit Code Review - Web Ui GWT Plugin Archetype</name>
 
   <properties>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
index 6e1ba21..3a28c48 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
@@ -41,6 +41,7 @@
           </includes>
           <archive>
             <manifestEntries>
+              <Gerrit-PluginName>${pluginName}</Gerrit-PluginName>
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
               <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
@@ -120,9 +121,9 @@
     <repository>
       <id>gerrit-api-repository</id>
 #if ($gerritApiVersion.endsWith("SNAPSHOT"))
-      <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+      <url>https://gerrit-api.storage.googleapis.com/snapshot/</url>
 #else
-      <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+      <url>https://gerrit-api.storage.googleapis.com/release/</url>
 #end
     </repository>
   </repositories>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 3d1b923..3c9e2ca 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -19,13 +19,9 @@
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
+  <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
+  <version>2.8-SNAPSHOT</version>
   <name>Gerrit Code Review - Plugin GWT UI</name>
 
   <description>
@@ -36,11 +32,13 @@
     <dependency>
       <groupId>com.google.gwt</groupId>
       <artifactId>gwt-user</artifactId>
+      <version>2.5.1</version>
     </dependency>
 
     <dependency>
       <groupId>com.google.gwt</groupId>
       <artifactId>gwt-dev</artifactId>
+      <version>2.5.1</version>
     </dependency>
   </dependencies>
 
@@ -119,6 +117,44 @@
           </execution>
         </executions>
       </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>unpack-sources</id>
+            <phase>package</phase>
+            <configuration>
+              <tasks>
+                <unzip src="${project.build.directory}/${project.artifactId}-${project.version}-sources.jar" dest="${project.build.directory}/unpack_sources" />
+              </tasks>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <configuration>
+          <sourcepath>${project.build.directory}/unpack_sources</sourcepath>
+          <encoding>ISO-8859-1</encoding>
+          <quiet>true</quiet>
+          <detectOfflineLinks>false</detectOfflineLinks>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+            <phase>package</phase>
+          </execution>
+        </executions>
+      </plugin>
+
     </plugins>
   </build>
 </project>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index 2972a8e..681d7a9 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -18,13 +18,9 @@
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
 
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
+  <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-js-archetype</artifactId>
+  <version>2.8-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
 
   <properties>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
index 2a8b469..85de7b5 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
@@ -42,6 +42,7 @@
           </includes>
           <archive>
             <manifestEntries>
+              <Gerrit-PluginName>${pluginName}</Gerrit-PluginName>
               <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
               <Implementation-URL>${Implementation-Url}</Implementation-URL>
 
@@ -112,9 +113,9 @@
     <repository>
       <id>gerrit-api-repository</id>
 #if ($gerritApiVersion.endsWith("SNAPSHOT"))
-      <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+      <url>https://gerrit-api.storage.googleapis.com/snapshot/</url>
 #else
-      <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+      <url>https://gerrit-api.storage.googleapis.com/release/</url>
 #end
     </repository>
   </repositories>
diff --git a/gerrit-prettify/BUCK b/gerrit-prettify/BUCK
new file mode 100644
index 0000000..79dc760
--- /dev/null
+++ b/gerrit-prettify/BUCK
@@ -0,0 +1,47 @@
+SRC = 'src/main/java/com/google/gerrit/prettify/'
+
+gwt_module(
+  name = 'client',
+  srcs = glob([
+    SRC + 'client/**/*.java',
+    SRC + 'common/**/*.java',
+  ]),
+  gwtxml = SRC + 'PrettyFormatter.gwt.xml',
+  resources = glob([
+    'src/main/java/com/google/gerrit/prettify/client/*.properties',
+  ]),
+  deps = [
+    ':google-code-prettify',
+    '//gerrit-patch-jgit:client',
+    '//gerrit-reviewdb:client',
+    '//gerrit-gwtexpui:SafeHtml',
+    '//lib:guava',
+    '//lib:gwtjsonrpc',
+    '//lib/gwt:user',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'google-code-prettify',
+  resources = glob([
+    'src/main/resources/com/google/gerrit/prettify/client/**/*',
+  ]),
+  deps = [
+    '//lib:LICENSE-Apache2.0',
+  ],
+)
+
+java_library(
+  name = 'server',
+  srcs = glob([SRC + 'common/**/*.java']),
+  deps = [
+    '//gerrit-patch-jgit:server',
+    '//gerrit-reviewdb:server',
+    '//lib:guava',
+    '//lib:gwtjsonrpc',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
deleted file mode 100644
index fafc399..0000000
--- a/gerrit-prettify/pom.xml
+++ /dev/null
@@ -1,76 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-prettify</artifactId>
-  <name>Gerrit Code Review - Prettify</name>
-
-  <description>
-    Prettify based syntax highlighting
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-gwtexpui</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-patch-jgit</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-reviewdb</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-user</artifactId>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
index 8e7c699..34ddde2 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
@@ -38,18 +38,27 @@
 
     prettify.compile(Resources.I.core());
     prettify.compile(Resources.I.lang_apollo());
+    prettify.compile(Resources.I.lang_basic());
     prettify.compile(Resources.I.lang_clj());
     prettify.compile(Resources.I.lang_css());
     prettify.compile(Resources.I.lang_dart());
+    prettify.compile(Resources.I.lang_erlang());
     prettify.compile(Resources.I.lang_go());
     prettify.compile(Resources.I.lang_hs());
     prettify.compile(Resources.I.lang_lisp());
+    prettify.compile(Resources.I.lang_llvm());
     prettify.compile(Resources.I.lang_lua());
+    prettify.compile(Resources.I.lang_matlab());
     prettify.compile(Resources.I.lang_ml());
+    prettify.compile(Resources.I.lang_mumps());
     prettify.compile(Resources.I.lang_n());
+    prettify.compile(Resources.I.lang_pascal());
     prettify.compile(Resources.I.lang_proto());
+    prettify.compile(Resources.I.lang_r());
+    prettify.compile(Resources.I.lang_rd());
     prettify.compile(Resources.I.lang_scala());
     prettify.compile(Resources.I.lang_sql());
+    prettify.compile(Resources.I.lang_tcl());
     prettify.compile(Resources.I.lang_tex());
     prettify.compile(Resources.I.lang_vb());
     prettify.compile(Resources.I.lang_vhdl());
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
index f53c91c..93c2988 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
@@ -33,18 +33,27 @@
   TextResource core();
 
   @Source("lang-apollo.js") TextResource lang_apollo();
+  @Source("lang-basic.js") TextResource lang_basic();
   @Source("lang-clj.js") TextResource lang_clj();
   @Source("lang-css.js") TextResource lang_css();
   @Source("lang-dart.js") TextResource lang_dart();
+  @Source("lang-erlang.js") TextResource lang_erlang();
   @Source("lang-go.js") TextResource lang_go();
   @Source("lang-hs.js") TextResource lang_hs();
   @Source("lang-lisp.js") TextResource lang_lisp();
+  @Source("lang-llvm.js") TextResource lang_llvm();
   @Source("lang-lua.js") TextResource lang_lua();
+  @Source("lang-matlab.js") TextResource lang_matlab();
   @Source("lang-ml.js") TextResource lang_ml();
+  @Source("lang-mumps.js") TextResource lang_mumps();
   @Source("lang-n.js") TextResource lang_n();
+  @Source("lang-pascal.js") TextResource lang_pascal();
   @Source("lang-proto.js") TextResource lang_proto();
+  @Source("lang-r.js") TextResource lang_r();
+  @Source("lang-rd.js") TextResource lang_rd();
   @Source("lang-scala.js") TextResource lang_scala();
   @Source("lang-sql.js") TextResource lang_sql();
+  @Source("lang-tcl.js") TextResource lang_tcl();
   @Source("lang-tex.js") TextResource lang_tex();
   @Source("lang-vb.js") TextResource lang_vb();
   @Source("lang-vhdl.js") TextResource lang_vhdl();
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-basic.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-basic.js
new file mode 100644
index 0000000..6b784d4
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-basic.js
@@ -0,0 +1,3 @@
+var a=null;
+PR.registerLangHandler(PR.createSimpleLexer([["str",/^"(?:[^\n\r"\\]|\\.)*(?:"|$)/,a,'"'],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["com",/^REM[^\n\r]*/,a],["kwd",/^\b(?:AND|CLOSE|CLR|CMD|CONT|DATA|DEF ?FN|DIM|END|FOR|GET|GOSUB|GOTO|IF|INPUT|LET|LIST|LOAD|NEW|NEXT|NOT|ON|OPEN|OR|POKE|PRINT|READ|RESTORE|RETURN|RUN|SAVE|STEP|STOP|SYS|THEN|TO|VERIFY|WAIT)\b/,a],["pln",/^[a-z][^\W_]?(?:\$|%)?/i,a],["lit",/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i,a,"0123456789"],["pun",
+/^.[^\s\w"$%.]*/,a]]),["basic","cbm"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
index 2086bd5..d7a4640 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
@@ -1,2 +1,2 @@
 PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n\u000c"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]+)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],
-["com",/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);
+["com",/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}\b/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-erlang.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-erlang.js
new file mode 100644
index 0000000..27214a5
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-erlang.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["lit",/^[a-z]\w*/],["lit",/^'(?:[^\n\f\r'\\]|\\[^&])+'?/,null,"'"],["lit",/^\?[^\t\n ({]+/,null,"?"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^%[^\n]*/],["kwd",/^(?:module|attributes|do|let|in|letrec|apply|call|primop|case|of|end|when|fun|try|catch|receive|after|char|integer|float,atom,string,var)\b/],
+["kwd",/^-[_a-z]+/],["typ",/^[A-Z_]\w*/],["pun",/^[,.;]/]]),["erlang","erl"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-llvm.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-llvm.js
new file mode 100644
index 0000000..16fade2
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-llvm.js
@@ -0,0 +1 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^!?"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["com",/^;[^\n\r]*/,null,";"]],[["pln",/^[!%@](?:[$\-.A-Z_a-z][\w$\-.]*|\d+)/],["kwd",/^[^\W\d]\w*/,null],["lit",/^\d+\.\d+/],["lit",/^(?:\d+|0[Xx][\dA-Fa-f]+)/],["pun",/^[(-*,:<->[\]{}]|\.\.\.$/]]),["llvm","ll"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-matlab.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-matlab.js
new file mode 100644
index 0000000..d0d3516
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-matlab.js
@@ -0,0 +1,6 @@
+var a=null,b=window.PR,c=[[b.PR_PLAIN,/^[\t-\r \xa0]+/,a," \t\r\n\u000b\u000c\u00a0"],[b.PR_COMMENT,/^%{[^%]*%+(?:[^%}][^%]*%+)*}/,a],[b.PR_COMMENT,/^%[^\n\r]*/,a,"%"],["syscmd",/^![^\n\r]*/,a,"!"]],d=[["linecont",/^\.\.\.\s*[\n\r]/,a],["err",/^\?\?\? [^\n\r]*/,a],["wrn",/^Warning: [^\n\r]*/,a],["codeoutput",/^>>\s+/,a],["codeoutput",/^octave:\d+>\s+/,a],["lang-matlab-operators",/^((?:[A-Za-z]\w*(?:\.[A-Za-z]\w*)*|[).\]}])')/,a],["lang-matlab-identifiers",/^([A-Za-z]\w*(?:\.[A-Za-z]\w*)*)(?!')/,a],
+[b.PR_STRING,/^'(?:[^']|'')*'/,a],[b.PR_LITERAL,/^[+-]?\.?\d+(?:\.\d*)?(?:[Ee][+-]?\d+)?[ij]?/,a],[b.PR_TAG,/^[()[\]{}]/,a],[b.PR_PUNCTUATION,/^[!&*-/:->@\\^|~]/,a]],e=[["lang-matlab-identifiers",/^([A-Za-z]\w*(?:\.[A-Za-z]\w*)*)/,a],[b.PR_TAG,/^[()[\]{}]/,a],[b.PR_PUNCTUATION,/^[!&*-/:->@\\^|~]/,a],["transpose",/^'/,a]];
+b.registerLangHandler(b.createSimpleLexer([],[[b.PR_KEYWORD,/^\b(?:break|case|catch|classdef|continue|else|elseif|end|for|function|global|if|otherwise|parfor|persistent|return|spmd|switch|try|while)\b/,a],["const",/^\b(?:true|false|inf|Inf|nan|NaN|eps|pi|ans|nargin|nargout|varargin|varargout)\b/,a],[b.PR_TYPE,/^\b(?:cell|struct|char|double|single|logical|u?int(?:8|16|32|64)|sparse)\b/,a],["fun",/^\b(?:abs|accumarray|acos(?:d|h)?|acot(?:d|h)?|acsc(?:d|h)?|actxcontrol(?:list|select)?|actxGetRunningServer|actxserver|addlistener|addpath|addpref|addtodate|airy|align|alim|all|allchild|alpha|alphamap|amd|ancestor|and|angle|annotation|any|area|arrayfun|asec(?:d|h)?|asin(?:d|h)?|assert|assignin|atan[2dh]?|audiodevinfo|audioplayer|audiorecorder|aufinfo|auread|autumn|auwrite|avifile|aviinfo|aviread|axes|axis|balance|bar(?:3|3h|h)?|base2dec|beep|BeginInvoke|bench|bessel[h-ky]|beta|betainc|betaincinv|betaln|bicg|bicgstab|bicgstabl|bin2dec|bitand|bitcmp|bitget|bitmax|bitnot|bitor|bitset|bitshift|bitxor|blanks|blkdiag|bone|box|brighten|brush|bsxfun|builddocsearchdb|builtin|bvp4c|bvp5c|bvpget|bvpinit|bvpset|bvpxtend|calendar|calllib|callSoapService|camdolly|cameratoolbar|camlight|camlookat|camorbit|campan|campos|camproj|camroll|camtarget|camup|camva|camzoom|cart2pol|cart2sph|cast|cat|caxis|cd|cdf2rdf|cdfepoch|cdfinfo|cdflib(?:.(?:close|closeVar|computeEpoch|computeEpoch16|create|createAttr|createVar|delete|deleteAttr|deleteAttrEntry|deleteAttrgEntry|deleteVar|deleteVarRecords|epoch16Breakdown|epochBreakdown|getAttrEntry|getAttrgEntry|getAttrMaxEntry|getAttrMaxgEntry|getAttrName|getAttrNum|getAttrScope|getCacheSize|getChecksum|getCompression|getCompressionCacheSize|getConstantNames|getConstantValue|getCopyright|getFileBackward|getFormat|getLibraryCopyright|getLibraryVersion|getMajority|getName|getNumAttrEntries|getNumAttrgEntries|getNumAttributes|getNumgAttributes|getReadOnlyMode|getStageCacheSize|getValidate|getVarAllocRecords|getVarBlockingFactor|getVarCacheSize|getVarCompression|getVarData|getVarMaxAllocRecNum|getVarMaxWrittenRecNum|getVarName|getVarNum|getVarNumRecsWritten|getVarPadValue|getVarRecordData|getVarReservePercent|getVarsMaxWrittenRecNum|getVarSparseRecords|getVersion|hyperGetVarData|hyperPutVarData|inquire|inquireAttr|inquireAttrEntry|inquireAttrgEntry|inquireVar|open|putAttrEntry|putAttrgEntry|putVarData|putVarRecordData|renameAttr|renameVar|setCacheSize|setChecksum|setCompression|setCompressionCacheSize|setFileBackward|setFormat|setMajority|setReadOnlyMode|setStageCacheSize|setValidate|setVarAllocBlockRecords|setVarBlockingFactor|setVarCacheSize|setVarCompression|setVarInitialRecs|setVarPadValue|SetVarReservePercent|setVarsCacheSize|setVarSparseRecords))?|cdfread|cdfwrite|ceil|cell2mat|cell2struct|celldisp|cellfun|cellplot|cellstr|cgs|checkcode|checkin|checkout|chol|cholinc|cholupdate|circshift|cla|clabel|class|clc|clear|clearvars|clf|clipboard|clock|close|closereq|cmopts|cmpermute|cmunique|colamd|colon|colorbar|colordef|colormap|colormapeditor|colperm|Combine|comet|comet3|commandhistory|commandwindow|compan|compass|complex|computer|cond|condeig|condest|coneplot|conj|containers.Map|contour(?:[3cf]|slice)?|contrast|conv|conv2|convhull|convhulln|convn|cool|copper|copyfile|copyobj|corrcoef|cos(?:d|h)?|cot(?:d|h)?|cov|cplxpair|cputime|createClassFromWsdl|createSoapMessage|cross|csc(?:d|h)?|csvread|csvwrite|ctranspose|cumprod|cumsum|cumtrapz|curl|customverctrl|cylinder|daqread|daspect|datacursormode|datatipinfo|date|datenum|datestr|datetick|datevec|dbclear|dbcont|dbdown|dblquad|dbmex|dbquit|dbstack|dbstatus|dbstep|dbstop|dbtype|dbup|dde23|ddeget|ddesd|ddeset|deal|deblank|dec2base|dec2bin|dec2hex|decic|deconv|del2|delaunay|delaunay3|delaunayn|DelaunayTri|delete|demo|depdir|depfun|det|detrend|deval|diag|dialog|diary|diff|diffuse|dir|disp|display|dither|divergence|dlmread|dlmwrite|dmperm|doc|docsearch|dos|dot|dragrect|drawnow|dsearch|dsearchn|dynamicprops|echo|echodemo|edit|eig|eigs|ellipj|ellipke|ellipsoid|empty|enableNETfromNetworkDrive|enableservice|EndInvoke|enumeration|eomday|eq|erf|erfc|erfcinv|erfcx|erfinv|error|errorbar|errordlg|etime|etree|etreeplot|eval|evalc|evalin|event.(?:EventData|listener|PropertyEvent|proplistener)|exifread|exist|exit|exp|expint|expm|expm1|export2wsdlg|eye|ezcontour|ezcontourf|ezmesh|ezmeshc|ezplot|ezplot3|ezpolar|ezsurf|ezsurfc|factor|factorial|fclose|feather|feature|feof|ferror|feval|fft|fft2|fftn|fftshift|fftw|fgetl|fgets|fieldnames|figure|figurepalette|fileattrib|filebrowser|filemarker|fileparts|fileread|filesep|fill|fill3|filter|filter2|find|findall|findfigs|findobj|findstr|finish|fitsdisp|fitsinfo|fitsread|fitswrite|fix|flag|flipdim|fliplr|flipud|floor|flow|fminbnd|fminsearch|fopen|format|fplot|fprintf|frame2im|fread|freqspace|frewind|fscanf|fseek|ftell|FTP|full|fullfile|func2str|functions|funm|fwrite|fzero|gallery|gamma|gammainc|gammaincinv|gammaln|gca|gcbf|gcbo|gcd|gcf|gco|ge|genpath|genvarname|get|getappdata|getenv|getfield|getframe|getpixelposition|getpref|ginput|gmres|gplot|grabcode|gradient|gray|graymon|grid|griddata(?:3|n)?|griddedInterpolant|gsvd|gt|gtext|guidata|guide|guihandles|gunzip|gzip|h5create|h5disp|h5info|h5read|h5readatt|h5write|h5writeatt|hadamard|handle|hankel|hdf|hdf5|hdf5info|hdf5read|hdf5write|hdfinfo|hdfread|hdftool|help|helpbrowser|helpdesk|helpdlg|helpwin|hess|hex2dec|hex2num|hgexport|hggroup|hgload|hgsave|hgsetget|hgtransform|hidden|hilb|hist|histc|hold|home|horzcat|hostid|hot|hsv|hsv2rgb|hypot|ichol|idivide|ifft|ifft2|ifftn|ifftshift|ilu|im2frame|im2java|imag|image|imagesc|imapprox|imfinfo|imformats|import|importdata|imread|imwrite|ind2rgb|ind2sub|inferiorto|info|inline|inmem|inpolygon|input|inputdlg|inputname|inputParser|inspect|instrcallback|instrfind|instrfindall|int2str|integral(?:2|3)?|interp(?:1|1q|2|3|ft|n)|interpstreamspeed|intersect|intmax|intmin|inv|invhilb|ipermute|isa|isappdata|iscell|iscellstr|ischar|iscolumn|isdir|isempty|isequal|isequaln|isequalwithequalnans|isfield|isfinite|isfloat|isglobal|ishandle|ishghandle|ishold|isinf|isinteger|isjava|iskeyword|isletter|islogical|ismac|ismatrix|ismember|ismethod|isnan|isnumeric|isobject|isocaps|isocolors|isonormals|isosurface|ispc|ispref|isprime|isprop|isreal|isrow|isscalar|issorted|isspace|issparse|isstr|isstrprop|isstruct|isstudent|isunix|isvarname|isvector|javaaddpath|javaArray|javachk|javaclasspath|javacomponent|javaMethod|javaMethodEDT|javaObject|javaObjectEDT|javarmpath|jet|keyboard|kron|lasterr|lasterror|lastwarn|lcm|ldivide|ldl|le|legend|legendre|length|libfunctions|libfunctionsview|libisloaded|libpointer|libstruct|license|light|lightangle|lighting|lin2mu|line|lines|linkaxes|linkdata|linkprop|linsolve|linspace|listdlg|listfonts|load|loadlibrary|loadobj|log|log10|log1p|log2|loglog|logm|logspace|lookfor|lower|ls|lscov|lsqnonneg|lsqr|lt|lu|luinc|magic|makehgtform|mat2cell|mat2str|material|matfile|matlab.io.MatFile|matlab.mixin.(?:Copyable|Heterogeneous(?:.getDefaultScalarElement)?)|matlabrc|matlabroot|max|maxNumCompThreads|mean|median|membrane|memmapfile|memory|menu|mesh|meshc|meshgrid|meshz|meta.(?:class(?:.fromName)?|DynamicProperty|EnumeratedValue|event|MetaData|method|package(?:.(?:fromName|getAllPackages))?|property)|metaclass|methods|methodsview|mex(?:.getCompilerConfigurations)?|MException|mexext|mfilename|min|minres|minus|mislocked|mkdir|mkpp|mldivide|mlint|mlintrpt|mlock|mmfileinfo|mmreader|mod|mode|more|move|movefile|movegui|movie|movie2avi|mpower|mrdivide|msgbox|mtimes|mu2lin|multibandread|multibandwrite|munlock|namelengthmax|nargchk|narginchk|nargoutchk|native2unicode|nccreate|ncdisp|nchoosek|ncinfo|ncread|ncreadatt|ncwrite|ncwriteatt|ncwriteschema|ndgrid|ndims|ne|NET(?:.(?:addAssembly|Assembly|convertArray|createArray|createGeneric|disableAutoRelease|enableAutoRelease|GenericClass|invokeGenericMethod|NetException|setStaticProperty))?|netcdf.(?:abort|close|copyAtt|create|defDim|defGrp|defVar|defVarChunking|defVarDeflate|defVarFill|defVarFletcher32|delAtt|endDef|getAtt|getChunkCache|getConstant|getConstantNames|getVar|inq|inqAtt|inqAttID|inqAttName|inqDim|inqDimID|inqDimIDs|inqFormat|inqGrpName|inqGrpNameFull|inqGrpParent|inqGrps|inqLibVers|inqNcid|inqUnlimDims|inqVar|inqVarChunking|inqVarDeflate|inqVarFill|inqVarFletcher32|inqVarID|inqVarIDs|open|putAtt|putVar|reDef|renameAtt|renameDim|renameVar|setChunkCache|setDefaultFormat|setFill|sync)|newplot|nextpow2|nnz|noanimate|nonzeros|norm|normest|not|notebook|now|nthroot|null|num2cell|num2hex|num2str|numel|nzmax|ode(?:113|15i|15s|23|23s|23t|23tb|45)|odeget|odeset|odextend|onCleanup|ones|open|openfig|opengl|openvar|optimget|optimset|or|ordeig|orderfields|ordqz|ordschur|orient|orth|pack|padecoef|pagesetupdlg|pan|pareto|parseSoapResponse|pascal|patch|path|path2rc|pathsep|pathtool|pause|pbaspect|pcg|pchip|pcode|pcolor|pdepe|pdeval|peaks|perl|perms|permute|pie|pink|pinv|planerot|playshow|plot|plot3|plotbrowser|plotedit|plotmatrix|plottools|plotyy|plus|pol2cart|polar|poly|polyarea|polyder|polyeig|polyfit|polyint|polyval|polyvalm|pow2|power|ppval|prefdir|preferences|primes|print|printdlg|printopt|printpreview|prod|profile|profsave|propedit|propertyeditor|psi|publish|PutCharArray|PutFullMatrix|PutWorkspaceData|pwd|qhull|qmr|qr|qrdelete|qrinsert|qrupdate|quad|quad2d|quadgk|quadl|quadv|questdlg|quit|quiver|quiver3|qz|rand|randi|randn|randperm|RandStream(?:.(?:create|getDefaultStream|getGlobalStream|list|setDefaultStream|setGlobalStream))?|rank|rat|rats|rbbox|rcond|rdivide|readasync|real|reallog|realmax|realmin|realpow|realsqrt|record|rectangle|rectint|recycle|reducepatch|reducevolume|refresh|refreshdata|regexp|regexpi|regexprep|regexptranslate|rehash|rem|Remove|RemoveAll|repmat|reset|reshape|residue|restoredefaultpath|rethrow|rgb2hsv|rgb2ind|rgbplot|ribbon|rmappdata|rmdir|rmfield|rmpath|rmpref|rng|roots|rose|rosser|rot90|rotate|rotate3d|round|rref|rsf2csf|run|save|saveas|saveobj|savepath|scatter|scatter3|schur|sec|secd|sech|selectmoveresize|semilogx|semilogy|sendmail|serial|set|setappdata|setdiff|setenv|setfield|setpixelposition|setpref|setstr|setxor|shading|shg|shiftdim|showplottool|shrinkfaces|sign|sin(?:d|h)?|size|slice|smooth3|snapnow|sort|sortrows|sound|soundsc|spalloc|spaugment|spconvert|spdiags|specular|speye|spfun|sph2cart|sphere|spinmap|spline|spones|spparms|sprand|sprandn|sprandsym|sprank|spring|sprintf|spy|sqrt|sqrtm|squeeze|ss2tf|sscanf|stairs|startup|std|stem|stem3|stopasync|str2double|str2func|str2mat|str2num|strcat|strcmp|strcmpi|stream2|stream3|streamline|streamparticles|streamribbon|streamslice|streamtube|strfind|strjust|strmatch|strncmp|strncmpi|strread|strrep|strtok|strtrim|struct2cell|structfun|strvcat|sub2ind|subplot|subsasgn|subsindex|subspace|subsref|substruct|subvolume|sum|summer|superclasses|superiorto|support|surf|surf2patch|surface|surfc|surfl|surfnorm|svd|svds|swapbytes|symamd|symbfact|symmlq|symrcm|symvar|system|tan(?:d|h)?|tar|tempdir|tempname|tetramesh|texlabel|text|textread|textscan|textwrap|tfqmr|throw|tic|Tiff(?:.(?:getTagNames|getVersion))?|timer|timerfind|timerfindall|times|timeseries|title|toc|todatenum|toeplitz|toolboxdir|trace|transpose|trapz|treelayout|treeplot|tril|trimesh|triplequad|triplot|TriRep|TriScatteredInterp|trisurf|triu|tscollection|tsearch|tsearchn|tstool|type|typecast|uibuttongroup|uicontextmenu|uicontrol|uigetdir|uigetfile|uigetpref|uiimport|uimenu|uiopen|uipanel|uipushtool|uiputfile|uiresume|uisave|uisetcolor|uisetfont|uisetpref|uistack|uitable|uitoggletool|uitoolbar|uiwait|uminus|undocheckout|unicode2native|union|unique|unix|unloadlibrary|unmesh|unmkpp|untar|unwrap|unzip|uplus|upper|urlread|urlwrite|usejava|userpath|validateattributes|validatestring|vander|var|vectorize|ver|verctrl|verLessThan|version|vertcat|VideoReader(?:.isPlatformSupported)?|VideoWriter(?:.getProfiles)?|view|viewmtx|visdiff|volumebounds|voronoi|voronoin|wait|waitbar|waitfor|waitforbuttonpress|warndlg|warning|waterfall|wavfinfo|wavplay|wavread|wavrecord|wavwrite|web|weekday|what|whatsnew|which|whitebg|who|whos|wilkinson|winopen|winqueryreg|winter|wk1finfo|wk1read|wk1write|workspace|xlabel|xlim|xlsfinfo|xlsread|xlswrite|xmlread|xmlwrite|xor|xslt|ylabel|ylim|zeros|zip|zlabel|zlim|zoom)\b/,
+a],["fun_tbx",/^\b(?:addedvarplot|andrewsplot|anova[12n]|ansaribradley|aoctool|barttest|bbdesign|beta(?:cdf|fit|inv|like|pdf|rnd|stat)|bino(?:cdf|fit|inv|pdf|rnd|stat)|biplot|bootci|bootstrp|boxplot|candexch|candgen|canoncorr|capability|capaplot|caseread|casewrite|categorical|ccdesign|cdfplot|chi2(?:cdf|gof|inv|pdf|rnd|stat)|cholcov|Classification(?:BaggedEnsemble|Discriminant(?:.(?:fit|make|template))?|Ensemble|KNN(?:.(?:fit|template))?|PartitionedEnsemble|PartitionedModel|Tree(?:.(?:fit|template))?)|classify|classregtree|cluster|clusterdata|cmdscale|combnk|Compact(?:Classification(?:Discriminant|Ensemble|Tree)|Regression(?:Ensemble|Tree)|TreeBagger)|confusionmat|controlchart|controlrules|cophenet|copula(?:cdf|fit|param|pdf|rnd|stat)|cordexch|corr|corrcov|coxphfit|createns|crosstab|crossval|cvpartition|datasample|dataset|daugment|dcovary|dendrogram|dfittool|disttool|dummyvar|dwtest|ecdf|ecdfhist|ev(?:cdf|fit|inv|like|pdf|rnd|stat)|ExhaustiveSearcher|exp(?:cdf|fit|inv|like|pdf|rnd|stat)|factoran|fcdf|ff2n|finv|fitdist|fitensemble|fpdf|fracfact|fracfactgen|friedman|frnd|fstat|fsurfht|fullfact|gagerr|gam(?:cdf|fit|inv|like|pdf|rnd|stat)|GeneralizedLinearModel(?:.fit)?|geo(?:cdf|inv|mean|pdf|rnd|stat)|gev(?:cdf|fit|inv|like|pdf|rnd|stat)|gline|glmfit|glmval|glyphplot|gmdistribution(?:.fit)?|gname|gp(?:cdf|fit|inv|like|pdf|rnd|stat)|gplotmatrix|grp2idx|grpstats|gscatter|haltonset|harmmean|hist3|histfit|hmm(?:decode|estimate|generate|train|viterbi)|hougen|hyge(?:cdf|inv|pdf|rnd|stat)|icdf|inconsistent|interactionplot|invpred|iqr|iwishrnd|jackknife|jbtest|johnsrnd|KDTreeSearcher|kmeans|knnsearch|kruskalwallis|ksdensity|kstest|kstest2|kurtosis|lasso|lassoglm|lassoPlot|leverage|lhsdesign|lhsnorm|lillietest|LinearModel(?:.fit)?|linhyptest|linkage|logn(?:cdf|fit|inv|like|pdf|rnd|stat)|lsline|mad|mahal|maineffectsplot|manova1|manovacluster|mdscale|mhsample|mle|mlecov|mnpdf|mnrfit|mnrnd|mnrval|moment|multcompare|multivarichart|mvn(?:cdf|pdf|rnd)|mvregress|mvregresslike|mvt(?:cdf|pdf|rnd)|NaiveBayes(?:.fit)?|nan(?:cov|max|mean|median|min|std|sum|var)|nbin(?:cdf|fit|inv|pdf|rnd|stat)|ncf(?:cdf|inv|pdf|rnd|stat)|nct(?:cdf|inv|pdf|rnd|stat)|ncx2(?:cdf|inv|pdf|rnd|stat)|NeighborSearcher|nlinfit|nlintool|nlmefit|nlmefitsa|nlparci|nlpredci|nnmf|nominal|NonLinearModel(?:.fit)?|norm(?:cdf|fit|inv|like|pdf|rnd|stat)|normplot|normspec|ordinal|outlierMeasure|parallelcoords|paretotails|partialcorr|pcacov|pcares|pdf|pdist|pdist2|pearsrnd|perfcurve|perms|piecewisedistribution|plsregress|poiss(?:cdf|fit|inv|pdf|rnd|tat)|polyconf|polytool|prctile|princomp|ProbDist(?:Kernel|Parametric|UnivKernel|UnivParam)?|probplot|procrustes|qqplot|qrandset|qrandstream|quantile|randg|random|randsample|randtool|range|rangesearch|ranksum|rayl(?:cdf|fit|inv|pdf|rnd|stat)|rcoplot|refcurve|refline|regress|Regression(?:BaggedEnsemble|Ensemble|PartitionedEnsemble|PartitionedModel|Tree(?:.(?:fit|template))?)|regstats|relieff|ridge|robustdemo|robustfit|rotatefactors|rowexch|rsmdemo|rstool|runstest|sampsizepwr|scatterhist|sequentialfs|signrank|signtest|silhouette|skewness|slicesample|sobolset|squareform|statget|statset|stepwise|stepwisefit|surfht|tabulate|tblread|tblwrite|tcdf|tdfread|tiedrank|tinv|tpdf|TreeBagger|treedisp|treefit|treeprune|treetest|treeval|trimmean|trnd|tstat|ttest|ttest2|unid(?:cdf|inv|pdf|rnd|stat)|unif(?:cdf|inv|it|pdf|rnd|stat)|vartest(?:2|n)?|wbl(?:cdf|fit|inv|like|pdf|rnd|stat)|wblplot|wishrnd|x2fx|xptread|zscore|ztest)\b/,
+a],["fun_tbx",/^\b(?:adapthisteq|analyze75info|analyze75read|applycform|applylut|axes2pix|bestblk|blockproc|bwarea|bwareaopen|bwboundaries|bwconncomp|bwconvhull|bwdist|bwdistgeodesic|bweuler|bwhitmiss|bwlabel|bwlabeln|bwmorph|bwpack|bwperim|bwselect|bwtraceboundary|bwulterode|bwunpack|checkerboard|col2im|colfilt|conndef|convmtx2|corner|cornermetric|corr2|cp2tform|cpcorr|cpselect|cpstruct2pairs|dct2|dctmtx|deconvblind|deconvlucy|deconvreg|deconvwnr|decorrstretch|demosaic|dicom(?:anon|dict|info|lookup|read|uid|write)|edge|edgetaper|entropy|entropyfilt|fan2para|fanbeam|findbounds|fliptform|freqz2|fsamp2|fspecial|ftrans2|fwind1|fwind2|getheight|getimage|getimagemodel|getline|getneighbors|getnhood|getpts|getrangefromclass|getrect|getsequence|gray2ind|graycomatrix|graycoprops|graydist|grayslice|graythresh|hdrread|hdrwrite|histeq|hough|houghlines|houghpeaks|iccfind|iccread|iccroot|iccwrite|idct2|ifanbeam|im2bw|im2col|im2double|im2int16|im2java2d|im2single|im2uint16|im2uint8|imabsdiff|imadd|imadjust|ImageAdapter|imageinfo|imagemodel|imapplymatrix|imattributes|imbothat|imclearborder|imclose|imcolormaptool|imcomplement|imcontour|imcontrast|imcrop|imdilate|imdisplayrange|imdistline|imdivide|imellipse|imerode|imextendedmax|imextendedmin|imfill|imfilter|imfindcircles|imfreehand|imfuse|imgca|imgcf|imgetfile|imhandles|imhist|imhmax|imhmin|imimposemin|imlincomb|imline|immagbox|immovie|immultiply|imnoise|imopen|imoverview|imoverviewpanel|impixel|impixelinfo|impixelinfoval|impixelregion|impixelregionpanel|implay|impoint|impoly|impositionrect|improfile|imputfile|impyramid|imreconstruct|imrect|imregconfig|imregionalmax|imregionalmin|imregister|imresize|imroi|imrotate|imsave|imscrollpanel|imshow|imshowpair|imsubtract|imtool|imtophat|imtransform|imview|ind2gray|ind2rgb|interfileinfo|interfileread|intlut|ippl|iptaddcallback|iptcheckconn|iptcheckhandle|iptcheckinput|iptcheckmap|iptchecknargin|iptcheckstrs|iptdemos|iptgetapi|iptGetPointerBehavior|iptgetpref|ipticondir|iptnum2ordinal|iptPointerManager|iptprefs|iptremovecallback|iptSetPointerBehavior|iptsetpref|iptwindowalign|iradon|isbw|isflat|isgray|isicc|isind|isnitf|isrgb|isrset|lab2double|lab2uint16|lab2uint8|label2rgb|labelmatrix|makecform|makeConstrainToRectFcn|makehdr|makelut|makeresampler|maketform|mat2gray|mean2|medfilt2|montage|nitfinfo|nitfread|nlfilter|normxcorr2|ntsc2rgb|openrset|ordfilt2|otf2psf|padarray|para2fan|phantom|poly2mask|psf2otf|qtdecomp|qtgetblk|qtsetblk|radon|rangefilt|reflect|regionprops|registration.metric.(?:MattesMutualInformation|MeanSquares)|registration.optimizer.(?:OnePlusOneEvolutionary|RegularStepGradientDescent)|rgb2gray|rgb2ntsc|rgb2ycbcr|roicolor|roifill|roifilt2|roipoly|rsetwrite|std2|stdfilt|strel|stretchlim|subimage|tformarray|tformfwd|tforminv|tonemap|translate|truesize|uintlut|viscircles|warp|watershed|whitepoint|wiener2|xyz2double|xyz2uint16|ycbcr2rgb)\b/,
+a],["fun_tbx",/^\b(?:bintprog|color|fgoalattain|fminbnd|fmincon|fminimax|fminsearch|fminunc|fseminf|fsolve|fzero|fzmult|gangstr|ktrlink|linprog|lsqcurvefit|lsqlin|lsqnonlin|lsqnonneg|optimget|optimset|optimtool|quadprog)\b/,a],["ident",/^[A-Za-z]\w*(?:\.[A-Za-z]\w*)*/,a]]),["matlab-identifiers"]);b.registerLangHandler(b.createSimpleLexer([],e),["matlab-operators"]);b.registerLangHandler(b.createSimpleLexer(c,d),["matlab"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-mumps.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-mumps.js
new file mode 100644
index 0000000..8a6b3fd
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-mumps.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"]|\\.)*"/,null,'"']],[["com",/^;[^\n\r]*/,null,";"],["dec",/^\$(?:d|device|ec|ecode|es|estack|et|etrap|h|horolog|i|io|j|job|k|key|p|principal|q|quit|st|stack|s|storage|sy|system|t|test|tl|tlevel|tr|trestart|x|y|z[a-z]*|a|ascii|c|char|d|data|e|extract|f|find|fn|fnumber|g|get|j|justify|l|length|na|name|o|order|p|piece|ql|qlength|qs|qsubscript|q|query|r|random|re|reverse|s|select|st|stack|t|text|tr|translate|nan)\b/i,
+null],["kwd",/^(?:[^$]b|break|c|close|d|do|e|else|f|for|g|goto|h|halt|h|hang|i|if|j|job|k|kill|l|lock|m|merge|n|new|o|open|q|quit|r|read|s|set|tc|tcommit|tre|trestart|tro|trollback|ts|tstart|u|use|v|view|w|write|x|xecute)\b/i,null],["lit",/^[+-]?(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?/i],["pln",/^[a-z][^\W_]*/i],["pun",/^[^\w\t\n\r"$%;^\xa0]|_/]]),["mumps"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-pascal.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-pascal.js
new file mode 100644
index 0000000..8435fad
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-pascal.js
@@ -0,0 +1,3 @@
+var a=null;
+PR.registerLangHandler(PR.createSimpleLexer([["str",/^'(?:[^\n\r'\\]|\\.)*(?:'|$)/,a,"'"],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["com",/^\(\*[\S\s]*?(?:\*\)|$)|^{[\S\s]*?(?:}|$)/,a],["kwd",/^(?:absolute|and|array|asm|assembler|begin|case|const|constructor|destructor|div|do|downto|else|end|external|for|forward|function|goto|if|implementation|in|inline|interface|interrupt|label|mod|not|object|of|or|packed|procedure|program|record|repeat|set|shl|shr|then|to|type|unit|until|uses|var|virtual|while|with|xor)\b/i,a],
+["lit",/^(?:true|false|self|nil)/i,a],["pln",/^[a-z][^\W_]*/i,a],["lit",/^(?:\$[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/i,a,"0123456789"],["pun",/^.[^\s\w$'./@]*/,a]]),["pascal"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-r.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-r.js
new file mode 100644
index 0000000..99af8f8
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-r.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^'\\]|\\[\S\s])*(?:'|$)/,null,"'"]],[["com",/^#.*/],["kwd",/^(?:if|else|for|while|repeat|in|next|break|return|switch|function)(?![\w.])/],["lit",/^0[Xx][\dA-Fa-f]+([Pp]\d+)?[Li]?/],["lit",/^[+-]?(\d+(\.\d+)?|\.\d+)([Ee][+-]?\d+)?[Li]?/],["lit",/^(?:NULL|NA(?:_(?:integer|real|complex|character)_)?|Inf|TRUE|FALSE|NaN|\.\.(?:\.|\d+))(?![\w.])/],
+["pun",/^(?:<<?-|->>?|-|==|<=|>=|<|>|&&?|!=|\|\|?|[!*+/^]|%.*?%|[$=@~]|:{1,3}|[(),;?[\]{}])/],["pln",/^(?:[A-Za-z]+[\w.]*|\.[^\W\d][\w.]*)(?![\w.])/],["str",/^`.+`/]]),["r","s","R","S","Splus"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-rd.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-rd.js
new file mode 100644
index 0000000..7a7e43f
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-rd.js
@@ -0,0 +1 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^%[^\n\r]*/,null,"%"]],[["lit",/^\\(?:cr|l?dots|R|tab)\b/],["kwd",/^\\[@-Za-z]+/],["kwd",/^#(?:ifn?def|endif)/],["pln",/^\\[{}]/],["pun",/^[()[\]{}]+/]]),["Rd","rd"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
index 2fddd3e..8ec4280 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
@@ -1,2 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|apply|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|following|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|merge|national|nocheck|nonclustered|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|percent|plan|preceding|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rows?|rule|save|schema|select|session_user|set|setuser|shutdown|some|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|unbounded|union|unique|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|writetext)(?=[^\w-]|$)/i,
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|apply|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|connect|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|following|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|matched|merge|natural|national|nocheck|nonclustered|nocycle|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|partition|percent|pivot|plan|preceding|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rows?|rule|save|schema|select|session_user|set|setuser|shutdown|some|start|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|unbounded|union|unique|unpivot|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|within|writetext|xml)(?=[^\w-]|$)/i,
 null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^[_a-z][\w-]*/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]]),["sql"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tcl.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tcl.js
new file mode 100644
index 0000000..490f562
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tcl.js
@@ -0,0 +1,3 @@
+var a=null;
+PR.registerLangHandler(PR.createSimpleLexer([["opn",/^{+/,a,"{"],["clo",/^}+/,a,"}"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:after|append|apply|array|break|case|catch|continue|error|eval|exec|exit|expr|for|foreach|if|incr|info|proc|return|set|switch|trace|uplevel|upvar|while)\b/,a],["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit",
+/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["tcl"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
index b151b7c..ddde464 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
@@ -1,2 +1,2 @@
 PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0\u2028\u2029]+/,null,"\t\n\r \u00a0\u2028\u2029"],["str",/^(?:["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})(?:["\u201c\u201d]c|$)|["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})*(?:["\u201c\u201d]|$))/i,null,'"\u201c\u201d'],["com",/^['\u2018\u2019](?:_(?:\r\n?|[^\r]?)|[^\n\r_\u2028\u2029])*/,null,"'\u2018\u2019"]],[["kwd",/^(?:addhandler|addressof|alias|and|andalso|ansi|as|assembly|auto|boolean|byref|byte|byval|call|case|catch|cbool|cbyte|cchar|cdate|cdbl|cdec|char|cint|class|clng|cobj|const|cshort|csng|cstr|ctype|date|decimal|declare|default|delegate|dim|directcast|do|double|each|else|elseif|end|endif|enum|erase|error|event|exit|finally|for|friend|function|get|gettype|gosub|goto|handles|if|implements|imports|in|inherits|integer|interface|is|let|lib|like|long|loop|me|mod|module|mustinherit|mustoverride|mybase|myclass|namespace|new|next|not|notinheritable|notoverridable|object|on|option|optional|or|orelse|overloads|overridable|overrides|paramarray|preserve|private|property|protected|public|raiseevent|readonly|redim|removehandler|resume|return|select|set|shadows|shared|short|single|static|step|stop|string|structure|sub|synclock|then|throw|to|try|typeof|unicode|until|variant|wend|when|while|with|withevents|writeonly|xor|endif|gosub|let|variant|wend)\b/i,
-null],["com",/^rem.*/i],["lit",/^(?:true\b|false\b|nothing\b|\d+(?:e[+-]?\d+[dfr]?|[dfilrs])?|(?:&h[\da-f]+|&o[0-7]+)[ils]?|\d*\.\d+(?:e[+-]?\d+)?[dfr]?|#\s+(?:\d+[/-]\d+[/-]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:am|pm))?)?|\d+:\d+(?::\d+)?(\s*(?:am|pm))?)\s+#)/i],["pln",/^(?:(?:[a-z]|_\w)\w*|\[(?:[a-z]|_\w)\w*])/i],["pun",/^[^\w\t\n\r "'[\]\xa0\u2018\u2019\u201c\u201d\u2028\u2029]+/],["pun",/^(?:\[|])/]]),["vb","vbs"]);
+null],["com",/^rem\b.*/i],["lit",/^(?:true\b|false\b|nothing\b|\d+(?:e[+-]?\d+[dfr]?|[dfilrs])?|(?:&h[\da-f]+|&o[0-7]+)[ils]?|\d*\.\d+(?:e[+-]?\d+)?[dfr]?|#\s+(?:\d+[/-]\d+[/-]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:am|pm))?)?|\d+:\d+(?::\d+)?(\s*(?:am|pm))?)\s+#)/i],["pln",/^(?:(?:[a-z]|_\w)\w*(?:\[[!#%&@]+])?|\[(?:[a-z]|_\w)\w*])/i],["pun",/^[^\w\t\n\r "'[\]\xa0\u2018\u2019\u201c\u201d\u2028\u2029]+/],["pun",/^(?:\[|])/]]),["vb","vbs"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js
index 4827bc3..7b99049 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js
@@ -1,28 +1,30 @@
-var r=null;window.PR_SHOULD_USE_CONTINUATION=!0;
-(function(){function O(a){function i(d){var a=d.charCodeAt(0);if(a!==92)return a;var f=d.charAt(1);return(a=s[f])?a:"0"<=f&&f<="7"?parseInt(d.substring(1),8):f==="u"||f==="x"?parseInt(d.substring(2),16):d.charCodeAt(1)}function g(d){if(d<32)return(d<16?"\\x0":"\\x")+d.toString(16);d=String.fromCharCode(d);return d==="\\"||d==="-"||d==="]"||d==="^"?"\\"+d:d}function j(d){var a=d.substring(1,d.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),d=[],f=
-a[0]==="^",b=["["];f&&b.push("^");for(var f=f?1:0,c=a.length;f<c;++f){var h=a[f];if(/\\[bdsw]/i.test(h))b.push(h);else{var h=i(h),e;f+2<c&&"-"===a[f+1]?(e=i(a[f+2]),f+=2):e=h;d.push([h,e]);e<65||h>122||(e<65||h>90||d.push([Math.max(65,h)|32,Math.min(e,90)|32]),e<97||h>122||d.push([Math.max(97,h)&-33,Math.min(e,122)&-33]))}}d.sort(function(d,a){return d[0]-a[0]||a[1]-d[1]});a=[];c=[];for(f=0;f<d.length;++f)h=d[f],h[0]<=c[1]+1?c[1]=Math.max(c[1],h[1]):a.push(c=h);for(f=0;f<a.length;++f)h=a[f],b.push(g(h[0])),
-h[1]>h[0]&&(h[1]+1>h[0]&&b.push("-"),b.push(g(h[1])));b.push("]");return b.join("")}function t(d){for(var a=d.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=a.length,i=[],c=0,h=0;c<b;++c){var e=a[c];e==="("?++h:"\\"===e.charAt(0)&&(e=+e.substring(1))&&(e<=h?i[e]=-1:a[c]=g(e))}for(c=1;c<i.length;++c)-1===i[c]&&(i[c]=++z);for(h=c=0;c<b;++c)e=a[c],e==="("?(++h,i[h]||(a[c]="(?:")):"\\"===e.charAt(0)&&(e=+e.substring(1))&&e<=h&&
-(a[c]="\\"+i[e]);for(c=0;c<b;++c)"^"===a[c]&&"^"!==a[c+1]&&(a[c]="");if(d.ignoreCase&&w)for(c=0;c<b;++c)e=a[c],d=e.charAt(0),e.length>=2&&d==="["?a[c]=j(e):d!=="\\"&&(a[c]=e.replace(/[A-Za-z]/g,function(d){d=d.charCodeAt(0);return"["+String.fromCharCode(d&-33,d|32)+"]"}));return a.join("")}for(var z=0,w=!1,k=!1,m=0,b=a.length;m<b;++m){var o=a[m];if(o.ignoreCase)k=!0;else if(/[a-z]/i.test(o.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){w=!0;k=!1;break}}for(var s={b:8,t:9,n:10,v:11,
-f:12,r:13},q=[],m=0,b=a.length;m<b;++m){o=a[m];if(o.global||o.multiline)throw Error(""+o);q.push("(?:"+t(o)+")")}return RegExp(q.join("|"),k?"gi":"g")}function P(a,i){function g(a){switch(a.nodeType){case 1:if(j.test(a.className))break;for(var b=a.firstChild;b;b=b.nextSibling)g(b);b=a.nodeName.toLowerCase();if("br"===b||"li"===b)t[k]="\n",w[k<<1]=z++,w[k++<<1|1]=a;break;case 3:case 4:b=a.nodeValue,b.length&&(b=i?b.replace(/\r\n?/g,"\n"):b.replace(/[\t\n\r ]+/g," "),t[k]=b,w[k<<1]=z,z+=b.length,w[k++<<
-1|1]=a)}}var j=/(?:^|\s)nocode(?:\s|$)/,t=[],z=0,w=[],k=0;g(a);return{a:t.join("").replace(/\n$/,""),d:w}}function E(a,i,g,j){i&&(a={a:i,e:a},g(a),j.push.apply(j,a.g))}function x(a,i){function g(a){for(var k=a.e,m=[k,"pln"],b=0,o=a.a.match(t)||[],s={},q=0,d=o.length;q<d;++q){var v=o[q],f=s[v],u=void 0,c;if(typeof f==="string")c=!1;else{var h=j[v.charAt(0)];if(h)u=v.match(h[1]),f=h[0];else{for(c=0;c<z;++c)if(h=i[c],u=v.match(h[1])){f=h[0];break}u||(f="pln")}if((c=f.length>=5&&"lang-"===f.substring(0,
-5))&&!(u&&typeof u[1]==="string"))c=!1,f="src";c||(s[v]=f)}h=b;b+=v.length;if(c){c=u[1];var e=v.indexOf(c),p=e+c.length;u[2]&&(p=v.length-u[2].length,e=p-c.length);f=f.substring(5);E(k+h,v.substring(0,e),g,m);E(k+h+e,c,F(f,c),m);E(k+h+p,v.substring(p),g,m)}else m.push(k+h,f)}a.g=m}var j={},t;(function(){for(var g=a.concat(i),k=[],m={},b=0,o=g.length;b<o;++b){var s=g[b],q=s[3];if(q)for(var d=q.length;--d>=0;)j[q.charAt(d)]=s;s=s[1];q=""+s;m.hasOwnProperty(q)||(k.push(s),m[q]=r)}k.push(/[\S\s]/);t=
-O(k)})();var z=i.length;return g}function l(a){var i=[],g=[];a.tripleQuotedStrings?i.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,r,"'\""]):a.multiLineStrings?i.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,r,"'\"`"]):i.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,r,"\"'"]);a.verbatimStrings&&
-g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,r]);var j=a.hashComments;j&&(a.cStyleComments?(j>1?i.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,r,"#"]):i.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,r,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,r])):i.push(["com",/^#[^\n\r]*/,r,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,r]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,
-r]));a.regexLiterals&&g.push(["lang-regex",/^(?:^^\.?|[+-]|[!=]={0,2}|#|%=?|&&?=?|\(|\*=?|[+-]=|->|\/=?|::?|<<?=?|>{1,3}=?|[,;?@[{~]|\^\^?=?|\|\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(j=a.types)&&g.push(["typ",j]);a=(""+a.keywords).replace(/^ | $/g,"");a.length&&g.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),r]);i.push(["pln",/^\s+/,r," \r\n\t\u00a0"]);g.push(["lit",
-/^@[$_a-z][\w$@]*/i,r],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,r],["pln",/^[$_a-z][\w$@]*/i,r],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,r,"0123456789"],["pln",/^\\[\S\s]?/,r],["pun",/^.[^\s\w"$'./@\\`]*/,r]);return x(i,g)}function G(a,i,g){function j(a){switch(a.nodeType){case 1:if(z.test(a.className))break;if("br"===a.nodeName)t(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)j(a);break;case 3:case 4:if(g){var b=
-a.nodeValue,f=b.match(n);if(f){var i=b.substring(0,f.index);a.nodeValue=i;(b=b.substring(f.index+f[0].length))&&a.parentNode.insertBefore(k.createTextNode(b),a.nextSibling);t(a);i||a.parentNode.removeChild(a)}}}}function t(a){function i(a,b){var d=b?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=i(e,1),f=a.nextSibling;e.appendChild(d);for(var g=f;g;g=f)f=g.nextSibling,e.appendChild(g)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=i(a.nextSibling,0),f;(f=a.parentNode)&&f.nodeType===
-1;)a=f;b.push(a)}for(var z=/(?:^|\s)nocode(?:\s|$)/,n=/\r\n?|\n/,k=a.ownerDocument,m=k.createElement("li");a.firstChild;)m.appendChild(a.firstChild);for(var b=[m],o=0;o<b.length;++o)j(b[o]);i===(i|0)&&b[0].setAttribute("value",i);var s=k.createElement("ol");s.className="linenums";for(var i=Math.max(0,i-1|0)||0,o=0,q=b.length;o<q;++o)m=b[o],m.className="L"+(o+i)%10,m.firstChild||m.appendChild(k.createTextNode("\u00a0")),s.appendChild(m);a.appendChild(s)}function n(a,i){for(var g=i.length;--g>=0;){var j=
-i[g];A.hasOwnProperty(j)?C.console&&console.warn("cannot override language handler %s",j):A[j]=a}}function F(a,i){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(i)?"default-markup":"default-code";return A[a]}function H(a){var i=a.h;try{var g=P(a.c,a.i),j=g.a;a.a=j;a.d=g.d;a.e=0;F(i,j)(a);var t=/\bMSIE\s(\d+)/.exec(navigator.userAgent),t=t&&+t[1]<=8,i=/\n/g,n=a.a,w=n.length,g=0,k=a.d,m=k.length,j=0,b=a.g,o=b.length,s=0;b[o]=w;var q,d;for(d=q=0;d<o;)b[d]!==b[d+2]?(b[q++]=b[d++],b[q++]=b[d++]):d+=2;o=q;
-for(d=q=0;d<o;){for(var v=b[d],f=b[d+1],u=d+2;u+2<=o&&b[u+1]===f;)u+=2;b[q++]=v;b[q++]=f;d=u}b.length=q;var c=a.c,h;if(c)h=c.style.display,c.style.display="none";try{for(;j<m;){var e=k[j+2]||w,p=b[s+2]||w,u=Math.min(e,p),l=k[j+1],D;if(l.nodeType!==1&&(D=n.substring(g,u))){t&&(D=D.replace(i,"\r"));l.nodeValue=D;var y=l.ownerDocument,x=y.createElement("span");x.className=b[s+1];var B=l.parentNode;B.replaceChild(x,l);x.appendChild(l);g<e&&(k[j+1]=l=y.createTextNode(n.substring(u,e)),B.insertBefore(l,
-x.nextSibling))}g=u;g>=e&&(j+=2);g>=p&&(s+=2)}}finally{if(c)c.style.display=h}}catch(A){C.console&&console.log(A&&A.stack?A.stack:A)}}var C=window,y=["break,continue,do,else,for,if,return,while"],B=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],I=[B,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],
-J=[B,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],K=[J,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],B=[B,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],
-L=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],M=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],N=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,
-Q=/\S/,R=l({keywords:[I,K,B,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+L,M,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};n(R,["default-code"]);n(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",
-/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);n(x([["pln",/^\s+/,r," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,r,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],
-["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);n(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);n(l({keywords:I,hashComments:!0,cStyleComments:!0,types:N}),["c","cc","cpp","cxx","cyc","m"]);n(l({keywords:"null,true,false"}),["json"]);n(l({keywords:K,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:N}),
-["cs"]);n(l({keywords:J,cStyleComments:!0}),["java"]);n(l({keywords:y,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);n(l({keywords:L,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py"]);n(l({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);n(l({keywords:M,hashComments:!0,
-multiLineStrings:!0,regexLiterals:!0}),["rb"]);n(l({keywords:B,cStyleComments:!0,regexLiterals:!0}),["js"]);n(l({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);n(x([],[["str",/^[\S\s]+/]]),["regex"]);var S=C.PR={createSimpleLexer:x,registerLangHandler:n,sourceDecorator:l,
-PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:C.prettyPrintOne=function(a,i,g){var j=document.createElement("pre");j.innerHTML=a;g&&G(j,g,!0);H({h:i,j:g,c:j,i:1});return j.innerHTML},prettyPrint:C.prettyPrint=function(a){function i(){var u;for(var g=C.PR_SHOULD_USE_CONTINUATION?k.now()+250:Infinity;m<j.length&&
-k.now()<g;m++){var c=j[m],h=c.className;if(s.test(h)&&!q.test(h)){for(var e=!1,p=c.parentNode;p;p=p.parentNode)if(f.test(p.tagName)&&p.className&&s.test(p.className)){e=!0;break}if(!e){c.className+=" prettyprinted";var h=h.match(o),n;if(e=!h){for(var e=c,p=void 0,l=e.firstChild;l;l=l.nextSibling)var t=l.nodeType,p=t===1?p?e:l:t===3?Q.test(l.nodeValue)?e:p:p;e=(n=p===e?void 0:p)&&v.test(n.tagName)}e&&(h=n.className.match(o));h&&(h=h[1]);u=d.test(c.tagName)?1:(e=(e=c.currentStyle)?e.whiteSpace:document.defaultView&&
-document.defaultView.getComputedStyle?document.defaultView.getComputedStyle(c,r).getPropertyValue("white-space"):0)&&"pre"===e.substring(0,3),e=u;(p=(p=c.className.match(/\blinenums\b(?::(\d+))?/))?p[1]&&p[1].length?+p[1]:!0:!1)&&G(c,p,e);b={h:h,c:c,j:p,i:e};H(b)}}}m<j.length?setTimeout(i,250):a&&a()}for(var g=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],j=[],n=0;n<g.length;++n)for(var l=0,w=g[n].length;l<w;++l)j.push(g[n][l]);var g=
-r,k=Date;k.now||(k={now:function(){return+new Date}});var m=0,b,o=/\blang(?:uage)?-([\w.]+)(?!\S)/,s=/\bprettyprint\b/,q=/\bprettyprinted\b/,d=/pre|xmp/i,v=/^code$/i,f=/^(?:pre|code|xmp)$/i;i()}};typeof define==="function"&&define.amd&&define("google-code-prettify",[],function(){return S})})();
+!function(){var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
+(function(){function S(a){function d(e){var b=e.charCodeAt(0);if(b!==92)return b;var a=e.charAt(1);return(b=r[a])?b:"0"<=a&&a<="7"?parseInt(e.substring(1),8):a==="u"||a==="x"?parseInt(e.substring(2),16):e.charCodeAt(1)}function g(e){if(e<32)return(e<16?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return e==="\\"||e==="-"||e==="]"||e==="^"?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),e=[],a=
+b[0]==="^",c=["["];a&&c.push("^");for(var a=a?1:0,f=b.length;a<f;++a){var h=b[a];if(/\\[bdsw]/i.test(h))c.push(h);else{var h=d(h),l;a+2<f&&"-"===b[a+1]?(l=d(b[a+2]),a+=2):l=h;e.push([h,l]);l<65||h>122||(l<65||h>90||e.push([Math.max(65,h)|32,Math.min(l,90)|32]),l<97||h>122||e.push([Math.max(97,h)&-33,Math.min(l,122)&-33]))}}e.sort(function(e,a){return e[0]-a[0]||a[1]-e[1]});b=[];f=[];for(a=0;a<e.length;++a)h=e[a],h[0]<=f[1]+1?f[1]=Math.max(f[1],h[1]):b.push(f=h);for(a=0;a<b.length;++a)h=b[a],c.push(g(h[0])),
+h[1]>h[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(g(h[1])));c.push("]");return c.join("")}function s(e){for(var a=e.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),c=a.length,d=[],f=0,h=0;f<c;++f){var l=a[f];l==="("?++h:"\\"===l.charAt(0)&&(l=+l.substring(1))&&(l<=h?d[l]=-1:a[f]=g(l))}for(f=1;f<d.length;++f)-1===d[f]&&(d[f]=++x);for(h=f=0;f<c;++f)l=a[f],l==="("?(++h,d[h]||(a[f]="(?:")):"\\"===l.charAt(0)&&(l=+l.substring(1))&&l<=h&&
+(a[f]="\\"+d[l]);for(f=0;f<c;++f)"^"===a[f]&&"^"!==a[f+1]&&(a[f]="");if(e.ignoreCase&&m)for(f=0;f<c;++f)l=a[f],e=l.charAt(0),l.length>=2&&e==="["?a[f]=b(l):e!=="\\"&&(a[f]=l.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var x=0,m=!1,j=!1,k=0,c=a.length;k<c;++k){var i=a[k];if(i.ignoreCase)j=!0;else if(/[a-z]/i.test(i.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){m=!0;j=!1;break}}for(var r={b:8,t:9,n:10,v:11,
+f:12,r:13},n=[],k=0,c=a.length;k<c;++k){i=a[k];if(i.global||i.multiline)throw Error(""+i);n.push("(?:"+s(i)+")")}return RegExp(n.join("|"),j?"gi":"g")}function T(a,d){function g(a){var c=a.nodeType;if(c==1){if(!b.test(a.className)){for(c=a.firstChild;c;c=c.nextSibling)g(c);c=a.nodeName.toLowerCase();if("br"===c||"li"===c)s[j]="\n",m[j<<1]=x++,m[j++<<1|1]=a}}else if(c==3||c==4)c=a.nodeValue,c.length&&(c=d?c.replace(/\r\n?/g,"\n"):c.replace(/[\t\n\r ]+/g," "),s[j]=c,m[j<<1]=x,x+=c.length,m[j++<<1|1]=
+a)}var b=/(?:^|\s)nocode(?:\s|$)/,s=[],x=0,m=[],j=0;g(a);return{a:s.join("").replace(/\n$/,""),d:m}}function H(a,d,g,b){d&&(a={a:d,e:a},g(a),b.push.apply(b,a.g))}function U(a){for(var d=void 0,g=a.firstChild;g;g=g.nextSibling)var b=g.nodeType,d=b===1?d?a:g:b===3?V.test(g.nodeValue)?a:d:d;return d===a?void 0:d}function C(a,d){function g(a){for(var j=a.e,k=[j,"pln"],c=0,i=a.a.match(s)||[],r={},n=0,e=i.length;n<e;++n){var z=i[n],w=r[z],t=void 0,f;if(typeof w==="string")f=!1;else{var h=b[z.charAt(0)];
+if(h)t=z.match(h[1]),w=h[0];else{for(f=0;f<x;++f)if(h=d[f],t=z.match(h[1])){w=h[0];break}t||(w="pln")}if((f=w.length>=5&&"lang-"===w.substring(0,5))&&!(t&&typeof t[1]==="string"))f=!1,w="src";f||(r[z]=w)}h=c;c+=z.length;if(f){f=t[1];var l=z.indexOf(f),B=l+f.length;t[2]&&(B=z.length-t[2].length,l=B-f.length);w=w.substring(5);H(j+h,z.substring(0,l),g,k);H(j+h+l,f,I(w,f),k);H(j+h+B,z.substring(B),g,k)}else k.push(j+h,w)}a.g=k}var b={},s;(function(){for(var g=a.concat(d),j=[],k={},c=0,i=g.length;c<i;++c){var r=
+g[c],n=r[3];if(n)for(var e=n.length;--e>=0;)b[n.charAt(e)]=r;r=r[1];n=""+r;k.hasOwnProperty(n)||(j.push(r),k[n]=q)}j.push(/[\S\s]/);s=S(j)})();var x=d.length;return g}function v(a){var d=[],g=[];a.tripleQuotedStrings?d.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?d.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
+q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var b=a.hashComments;b&&(a.cStyleComments?(b>1?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,q])):d.push(["com",
+/^#[^\n\r]*/,q,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,q]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));if(b=a.regexLiterals){var s=(b=b>1?"":"\n\r")?".":"[\\S\\s]";g.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<<?=?|>>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+s+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+
+s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,
+q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d=
+c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i<c.length;++i)b(c[i]);d===(d|0)&&c[0].setAttribute("value",d);var r=j.createElement("ol");
+r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.className="L"+(i+d)%10,k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a;
+a.a=b;a.d=g.d;a.e=0;I(d,b)(a);var s=/\bMSIE\s(\d+)/.exec(navigator.userAgent),s=s&&+s[1]<=8,d=/\n/g,x=a.a,m=x.length,g=0,j=a.d,k=j.length,b=0,c=a.g,i=c.length,r=0;c[i]=m;var n,e;for(e=n=0;e<i;)c[e]!==c[e+2]?(c[n++]=c[e++],c[n++]=c[e++]):e+=2;i=n;for(e=n=0;e<i;){for(var p=c[e],w=c[e+1],t=e+2;t+2<=i&&c[t+1]===w;)t+=2;c[n++]=p;c[n++]=w;e=t}c.length=n;var f=a.c,h;if(f)h=f.style.display,f.style.display="none";try{for(;b<k;){var l=j[b+2]||m,B=c[r+2]||m,t=Math.min(l,B),A=j[b+1],G;if(A.nodeType!==1&&(G=x.substring(g,
+t))){s&&(G=G.replace(d,"\r"));A.nodeValue=G;var L=A.ownerDocument,o=L.createElement("span");o.className=c[r+1];var v=A.parentNode;v.replaceChild(o,A);o.appendChild(A);g<l&&(j[b+1]=A=L.createTextNode(x.substring(t,l)),v.insertBefore(A,o.nextSibling))}g=t;g>=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
+"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],
+O=[N,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],E=[E,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],P=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
+Q=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],W=[y,"as,assert,const,copy,drop,enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv,pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],R=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,
+V=/\S/,X=v({keywords:[M,O,E,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",P,Q,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),F={};p(X,["default-code"]);p(C([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",
+/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);p(C([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],
+["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);p(C([],[["atv",/^[\S\s]+/]]),["uq.val"]);p(v({keywords:M,hashComments:!0,cStyleComments:!0,types:R}),["c","cc","cpp","cxx","cyc","m"]);p(v({keywords:"null,true,false"}),["json"]);p(v({keywords:O,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:R}),
+["cs"]);p(v({keywords:N,cStyleComments:!0}),["java"]);p(v({keywords:y,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);p(v({keywords:P,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);p(v({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);p(v({keywords:Q,
+hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);p(v({keywords:E,cStyleComments:!0,regexLiterals:!0}),["javascript","js"]);p(v({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);p(v({keywords:W,cStyleComments:!0,multilineStrings:!0}),["rc","rs","rust"]);
+p(C([],[["str",/^[\S\s]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:C,registerLangHandler:p,sourceDecorator:v,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,g){var b=document.createElement("div");b.innerHTML="<pre>"+a+"</pre>";b=b.firstChild;g&&J(b,g,!0);K({h:d,j:g,c:b,i:1});
+return b.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function g(){for(var b=D.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;i<p.length&&c.now()<b;i++){for(var d=p[i],j=h,k=d;k=k.previousSibling;){var m=k.nodeType,o=(m===7||m===8)&&k.nodeValue;if(o?!/^\??prettify\b/.test(o):m!==3||/\S/.test(k.nodeValue))break;if(o){j={};o.replace(/\b(\w+)=([\w%+\-.:]+)/g,function(a,b,c){j[b]=c});break}}k=d.className;if((j!==h||e.test(k))&&!v.test(k)){m=!1;for(o=d.parentNode;o;o=o.parentNode)if(f.test(o.tagName)&&
+o.className&&e.test(o.className)){m=!0;break}if(!m){d.className+=" prettyprinted";m=j.lang;if(!m){var m=k.match(n),y;if(!m&&(y=U(d))&&t.test(y.tagName))m=y.className.match(n);m&&(m=m[1])}if(w.test(d.tagName))o=1;else var o=d.currentStyle,u=s.defaultView,o=(o=o?o.whiteSpace:u&&u.getComputedStyle?u.getComputedStyle(d,q).getPropertyValue("white-space"):0)&&"pre"===o.substring(0,3);u=j.linenums;if(!(u=u==="true"||+u))u=(u=k.match(/\blinenums\b(?::(\d+))?/))?u[1]&&u[1].length?+u[1]:!0:!1;u&&J(d,u,o);r=
+{h:m,c:d,j:u,i:o};K(r)}}}i<p.length?setTimeout(g,250):"function"===typeof a&&a()}for(var b=d||document.body,s=b.ownerDocument||document,b=[b.getElementsByTagName("pre"),b.getElementsByTagName("code"),b.getElementsByTagName("xmp")],p=[],m=0;m<b.length;++m)for(var j=0,k=b[m].length;j<k;++j)p.push(b[m][j]);var b=q,c=Date;c.now||(c={now:function(){return+new Date}});var i=0,r,n=/\blang(?:uage)?-([\w.]+)(?!\S)/,e=/\bprettyprint\b/,v=/\bprettyprinted\b/,w=/pre|xmp/i,t=/^code$/i,f=/^(?:pre|code|xmp)$/i,
+h={};g()}};typeof define==="function"&&define.amd&&define("google-code-prettify",[],function(){return Y})})();}()
diff --git a/gerrit-reviewdb/BUCK b/gerrit-reviewdb/BUCK
new file mode 100644
index 0000000..05674cf
--- /dev/null
+++ b/gerrit-reviewdb/BUCK
@@ -0,0 +1,20 @@
+SRC = 'src/main/java/com/google/gerrit/reviewdb/'
+
+gwt_module(
+  name = 'client',
+  srcs = glob([SRC + 'client/**/*.java']),
+  gwtxml = SRC + 'ReviewDB.gwt.xml',
+  deps = [
+    '//lib:gwtorm',
+    '//lib:gwtorm_src'
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'server',
+  srcs = glob([SRC + '**/*.java']),
+  resources = glob(['src/main/resources/**/*']),
+  deps = ['//lib:gwtorm'],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
deleted file mode 100644
index 5543c53..0000000
--- a/gerrit-reviewdb/pom.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-reviewdb</artifactId>
-  <name>Gerrit Code Review - ReviewDB</name>
-
-  <description>
-    Database schema definition and interface.
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>gwtorm</groupId>
-      <artifactId>gwtorm</artifactId>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index 94b37e1..5cfa5e1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -25,14 +25,10 @@
  * A user may have multiple identities they can use to login to Gerrit (see
  * {@link AccountExternalId}), but in such cases they always map back to a
  * single Account entity.
- *<p>
+ * <p>
  * Entities "owned" by an Account (that is, their primary key contains the
  * {@link Account.Id} key as part of their key structure):
  * <ul>
- * <li>{@link AccountAgreement}: any record of the user's acceptance of a
- * predefined {@link ContributorAgreement}. Multiple records indicate
- * potentially multiple agreements, especially if agreements must be retired and
- * replaced with new agreements.</li>
  *
  * <li>{@link AccountExternalId}: OpenID identities and email addresses known to
  * be registered to this user. Multiple records can exist when the user has more
@@ -148,10 +144,11 @@
    *
    * @param newId unique id, see
    *        {@link com.google.gerrit.reviewdb.server.ReviewDb#nextAccountId()}.
+   * @param registeredOn when the account was registered.
    */
-  public Account(final Account.Id newId) {
-    accountId = newId;
-    registeredOn = new Timestamp(System.currentTimeMillis());
+  public Account(Account.Id newId, Timestamp registeredOn) {
+    this.accountId = newId;
+    this.registeredOn = registeredOn;
 
     generalPreferences = new AccountGeneralPreferences();
     generalPreferences.resetToDefaults();
@@ -207,8 +204,8 @@
     return contactFiledOn;
   }
 
-  public void setContactFiled() {
-    contactFiledOn = new Timestamp(System.currentTimeMillis());
+  public void setContactFiled(Timestamp ts) {
+    contactFiledOn = ts;
   }
 
   public boolean isActive() {
@@ -228,4 +225,14 @@
   public void setUserName(final String userName) {
     this.userName = userName;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    return o instanceof Account && ((Account) o).getId().equals(getId());
+  }
+
+  @Override
+  public int hashCode() {
+    return getId().get();
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index ad0f130..6cc83e5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -27,7 +27,7 @@
 
   /** Preferred scheme type to download a change. */
   public static enum DownloadScheme {
-    ANON_GIT, ANON_HTTP, ANON_SSH, HTTP, SSH, REPO_DOWNLOAD, DEFAULT_DOWNLOADS;
+    ANON_GIT, ANON_HTTP, HTTP, SSH, REPO_DOWNLOAD, DEFAULT_DOWNLOADS;
   }
 
   /** Preferred method to download a change. */
@@ -72,6 +72,16 @@
     EXPAND_ALL;
   }
 
+  public static enum DiffView {
+    SIDE_BY_SIDE,
+    UNIFIED_DIFF
+  }
+
+  public static enum ChangeScreen {
+    OLD_UI,
+    CHANGE_SCREEN2
+  }
+
   public static enum TimeFormat {
     /** 12-hour clock: 1:15 am, 2:13 pm */
     HHMM_12("h:mm a"),
@@ -128,7 +138,7 @@
   protected boolean reversePatchSetOrder;
 
   @Column(id = 11)
-  protected boolean showUsernameInReviewCategory;
+  protected boolean showUserInReview;
 
   @Column(id = 12)
   protected boolean relativeDateInChangeTable;
@@ -136,6 +146,12 @@
   @Column(id = 13, length = 20, notNull = false)
   protected String commentVisibilityStrategy;
 
+  @Column(id = 14, length = 20, notNull = false)
+  protected String diffView;
+
+  @Column(id = 15, length = 20, notNull = false)
+  protected String changeScreen;
+
   public AccountGeneralPreferences() {
   }
 
@@ -210,11 +226,11 @@
   }
 
   public boolean isShowUsernameInReviewCategory() {
-    return showUsernameInReviewCategory;
+    return showUserInReview;
   }
 
   public void setShowUsernameInReviewCategory(final boolean showUsernameInReviewCategory) {
-    this.showUsernameInReviewCategory = showUsernameInReviewCategory;
+    this.showUserInReview = showUsernameInReviewCategory;
   }
 
   public DateFormat getDateFormat() {
@@ -249,7 +265,7 @@
 
   public CommentVisibilityStrategy getCommentVisibilityStrategy() {
     if (commentVisibilityStrategy == null) {
-      return CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+      return CommentVisibilityStrategy.EXPAND_RECENT;
     }
     return CommentVisibilityStrategy.valueOf(commentVisibilityStrategy);
   }
@@ -259,17 +275,39 @@
     commentVisibilityStrategy = strategy.name();
   }
 
+  public DiffView getDiffView() {
+    if (diffView == null) {
+      return DiffView.SIDE_BY_SIDE;
+    }
+    return DiffView.valueOf(diffView);
+  }
+
+  public void setDiffView(DiffView diffView) {
+    this.diffView = diffView.name();
+  }
+
+  public ChangeScreen getChangeScreen() {
+    return changeScreen != null ? ChangeScreen.valueOf(changeScreen) : null;
+  }
+
+  public void setChangeScreen(ChangeScreen ui) {
+    changeScreen = ui != null ? ui.name() : null;
+  }
+
   public void resetToDefaults() {
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
     useFlashClipboard = true;
     copySelfOnEmail = false;
     reversePatchSetOrder = false;
-    showUsernameInReviewCategory = false;
+    showUserInReview = false;
     downloadUrl = null;
     downloadCommand = null;
     dateFormat = null;
     timeFormat = null;
     relativeDateInChangeTable = false;
+    commentVisibilityStrategy = null;
+    diffView = null;
+    changeScreen = null;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
new file mode 100644
index 0000000..3443f80
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.CompoundKey;
+
+/** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
+public final class AccountGroupById {
+  public static class Key extends CompoundKey<AccountGroup.Id> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1)
+    protected AccountGroup.Id groupId;
+
+    @Column(id = 2)
+    protected AccountGroup.UUID includeUUID;
+
+    protected Key() {
+      groupId = new AccountGroup.Id();
+      includeUUID = new AccountGroup.UUID();
+    }
+
+    public Key(final AccountGroup.Id g, final AccountGroup.UUID u) {
+      groupId = g;
+      includeUUID = u;
+    }
+
+    @Override
+    public AccountGroup.Id getParentKey() {
+      return groupId;
+    }
+
+    public AccountGroup.Id getGroupId() {
+      return groupId;
+    }
+
+    public AccountGroup.UUID getIncludeUUID() {
+      return includeUUID;
+    }
+
+    @Override
+    public com.google.gwtorm.client.Key<?>[] members() {
+      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
+    }
+  }
+
+  @Column(id = 1, name = Column.NONE)
+  protected Key key;
+
+  protected AccountGroupById() {
+  }
+
+  public AccountGroupById(final AccountGroupById.Key k) {
+    key = k;
+  }
+
+  public AccountGroupById.Key getKey() {
+    return key;
+  }
+
+  public AccountGroup.Id getGroupId() {
+    return key.groupId;
+  }
+
+  public AccountGroup.UUID getIncludeUUID() {
+    return key.includeUUID;
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
new file mode 100644
index 0000000..07e7d03
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.CompoundKey;
+
+import java.sql.Timestamp;
+
+/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
+public final class AccountGroupByIdAud {
+  public static class Key extends CompoundKey<AccountGroup.Id> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1)
+    protected AccountGroup.Id groupId;
+
+    @Column(id = 2)
+    protected AccountGroup.UUID includeUUID;
+
+    @Column(id = 3)
+    protected Timestamp addedOn;
+
+    protected Key() {
+      groupId = new AccountGroup.Id();
+      includeUUID = new AccountGroup.UUID();
+    }
+
+    public Key(final AccountGroup.Id g, final AccountGroup.UUID u, final Timestamp t) {
+      groupId = g;
+      includeUUID = u;
+      addedOn = t;
+    }
+
+    @Override
+    public AccountGroup.Id getParentKey() {
+      return groupId;
+    }
+
+    public AccountGroup.UUID getIncludeUUID() {
+      return includeUUID;
+    }
+
+    public Timestamp getAddedOn() {
+      return addedOn;
+    }
+
+    @Override
+    public com.google.gwtorm.client.Key<?>[] members() {
+      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
+    }
+  }
+
+  @Column(id = 1, name = Column.NONE)
+  protected Key key;
+
+  @Column(id = 2)
+  protected Account.Id addedBy;
+
+  @Column(id = 3, notNull = false)
+  protected Account.Id removedBy;
+
+  @Column(id = 4, notNull = false)
+  protected Timestamp removedOn;
+
+  protected AccountGroupByIdAud() {
+  }
+
+  public AccountGroupByIdAud(final AccountGroupById m,
+      final Account.Id adder, final Timestamp when) {
+    final AccountGroup.Id group = m.getGroupId();
+    final AccountGroup.UUID include = m.getIncludeUUID();
+    key = new AccountGroupByIdAud.Key(group, include, when);
+    addedBy = adder;
+  }
+
+  public AccountGroupByIdAud.Key getKey() {
+    return key;
+  }
+
+  public boolean isActive() {
+    return removedOn == null;
+  }
+
+  public void removed(final Account.Id deleter, final Timestamp when) {
+    removedBy = deleter;
+    removedOn = when;
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
deleted file mode 100644
index a5b35ed..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-/** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
-public final class AccountGroupIncludeByUuid {
-  public static class Key extends CompoundKey<AccountGroup.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected AccountGroup.Id groupId;
-
-    @Column(id = 2)
-    protected AccountGroup.UUID includeUUID;
-
-    protected Key() {
-      groupId = new AccountGroup.Id();
-      includeUUID = new AccountGroup.UUID();
-    }
-
-    public Key(final AccountGroup.Id g, final AccountGroup.UUID u) {
-      groupId = g;
-      includeUUID = u;
-    }
-
-    @Override
-    public AccountGroup.Id getParentKey() {
-      return groupId;
-    }
-
-    public AccountGroup.Id getGroupId() {
-      return groupId;
-    }
-
-    public AccountGroup.UUID getIncludeUUID() {
-      return includeUUID;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  protected AccountGroupIncludeByUuid() {
-  }
-
-  public AccountGroupIncludeByUuid(final AccountGroupIncludeByUuid.Key k) {
-    key = k;
-  }
-
-  public AccountGroupIncludeByUuid.Key getKey() {
-    return key;
-  }
-
-  public AccountGroup.Id getGroupId() {
-    return key.groupId;
-  }
-
-  public AccountGroup.UUID getIncludeUUID() {
-    return key.includeUUID;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
deleted file mode 100644
index 6625197..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
-public final class AccountGroupIncludeByUuidAudit {
-  public static class Key extends CompoundKey<AccountGroup.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected AccountGroup.Id groupId;
-
-    @Column(id = 2)
-    protected AccountGroup.UUID includeUUID;
-
-    @Column(id = 3)
-    protected Timestamp addedOn;
-
-    protected Key() {
-      groupId = new AccountGroup.Id();
-      includeUUID = new AccountGroup.UUID();
-    }
-
-    public Key(final AccountGroup.Id g, final AccountGroup.UUID u, final Timestamp t) {
-      groupId = g;
-      includeUUID = u;
-      addedOn = t;
-    }
-
-    @Override
-    public AccountGroup.Id getParentKey() {
-      return groupId;
-    }
-
-    public AccountGroup.UUID getIncludeUUID() {
-      return includeUUID;
-    }
-
-    public Timestamp getAddedOn() {
-      return addedOn;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  @Column(id = 2)
-  protected Account.Id addedBy;
-
-  @Column(id = 3, notNull = false)
-  protected Account.Id removedBy;
-
-  @Column(id = 4, notNull = false)
-  protected Timestamp removedOn;
-
-  protected AccountGroupIncludeByUuidAudit() {
-  }
-
-  public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
-      final Account.Id adder, final Timestamp when) {
-    final AccountGroup.Id group = m.getGroupId();
-    final AccountGroup.UUID include = m.getIncludeUUID();
-    key = new AccountGroupIncludeByUuidAudit.Key(group, include, when);
-    addedBy = adder;
-  }
-
-  public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
-      final Account.Id adder) {
-    this(m, adder, now());
-  }
-
-  public AccountGroupIncludeByUuidAudit.Key getKey() {
-    return key;
-  }
-
-  public boolean isActive() {
-    return removedOn == null;
-  }
-
-  public void removed(final Account.Id deleter) {
-    removedBy = deleter;
-    removedOn = now();
-  }
-
-  public void removed(final Account.Id deleter, final Timestamp when) {
-    removedBy = deleter;
-    removedOn = when;
-  }
-
-  private static Timestamp now() {
-    return new Timestamp(System.currentTimeMillis());
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index 523134b..d3798db 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -79,11 +79,6 @@
   }
 
   public AccountGroupMemberAudit(final AccountGroupMember m,
-      final Account.Id adder) {
-    this(m, adder, now());
-  }
-
-  public AccountGroupMemberAudit(final AccountGroupMember m,
       final Account.Id adder, Timestamp addedOn) {
     final Account.Id who = m.getAccountId();
     final AccountGroup.Id group = m.getAccountGroupId();
@@ -99,17 +94,13 @@
     return removedOn == null;
   }
 
-  public void removed(final Account.Id deleter) {
+  public void removed(final Account.Id deleter, final Timestamp when) {
     removedBy = deleter;
-    removedOn = now();
+    removedOn = when;
   }
 
   public void removedLegacy() {
     removedBy = addedBy;
     removedOn = key.addedOn;
   }
-
-  private static Timestamp now() {
-    return new Timestamp(System.currentTimeMillis());
-  }
 }
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 2a51872..76a38b9 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
@@ -388,11 +388,11 @@
   protected Change() {
   }
 
-  public Change(final Change.Key newKey, final Change.Id newId,
-      final Account.Id ownedBy, final Branch.NameKey forBranch) {
+  public Change(Change.Key newKey, Change.Id newId, Account.Id ownedBy,
+      Branch.NameKey forBranch, Timestamp ts) {
     changeKey = newKey;
     changeId = newId;
-    createdOn = new Timestamp(System.currentTimeMillis());
+    createdOn = ts;
     lastUpdatedOn = createdOn;
     owner = ownedBy;
     dest = forBranch;
@@ -431,8 +431,8 @@
     lastUpdatedOn = now;
   }
 
-  public void resetLastUpdatedOn() {
-    lastUpdatedOn = new Timestamp(System.currentTimeMillis());
+  public int getRowVersion() {
+    return rowVersion;
   }
 
   public String getSortKey() {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
index e05f5e7..b98104a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
@@ -78,11 +78,6 @@
   }
 
   public ChangeMessage(final ChangeMessage.Key k, final Account.Id a,
-      final PatchSet.Id psid) {
-    this(k, a, new Timestamp(System.currentTimeMillis()), psid);
-  }
-
-  public ChangeMessage(final ChangeMessage.Key k, final Account.Id a,
       final Timestamp wo, final PatchSet.Id psid) {
     key = k;
     author = a;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
new file mode 100644
index 0000000..b1b2615
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
@@ -0,0 +1,99 @@
+// 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.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+
+public class CommentRange {
+
+  @Column(id = 1)
+  protected int startLine;
+
+  @Column(id = 2)
+  protected int startCharacter;
+
+  @Column(id = 3)
+  protected int endLine;
+
+  @Column(id = 4)
+  protected int endCharacter;
+
+  protected CommentRange() {
+  }
+
+  public CommentRange(int sl, int sc, int el, int ec) {
+    startLine = sl;
+    startCharacter = sc;
+    endLine = el;
+    endCharacter = ec;
+  }
+
+  public int getStartLine() {
+    return startLine;
+  }
+
+  public int getStartCharacter() {
+    return startCharacter;
+  }
+
+  public int getEndLine() {
+    return endLine;
+  }
+
+  public int getEndCharacter() {
+    return endCharacter;
+  }
+
+  public void setStartLine(int sl) {
+    startLine = sl;
+  }
+
+  public void setStartCharacter(int sc) {
+    startCharacter = sc;
+  }
+
+  public void setEndLine(int el) {
+    endLine = el;
+  }
+
+  public void setEndCharacter(int ec) {
+    endCharacter = ec;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof CommentRange) {
+      CommentRange other = (CommentRange) obj;
+      return startLine == other.startLine && startCharacter == other.startCharacter &&
+          endLine == other.endLine && endCharacter == other.endCharacter;
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    int h = startLine;
+    h = h * 31 + startCharacter;
+    h = h * 31 + endLine;
+    h = h * 31 + endCharacter;
+    return h;
+  }
+
+  @Override
+  public String toString() {
+    return "Range[startLine=" + startLine + ", startCharacter=" + startCharacter
+        + ", endLine=" + endLine + ", endCharacter=" + endCharacter + "]";
+  }
+}
\ No newline at end of file
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index 6ddd6d2..f5ecd2e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -97,6 +97,10 @@
       return code;
     }
 
+    public boolean matches(String s) {
+      return s != null && s.length() == 1 && s.charAt(0) == code;
+    }
+
     public static ChangeType forCode(final char c) {
       for (final ChangeType s : ChangeType.values()) {
         if (s.code == c) {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index af35e52f..916219b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -117,17 +117,20 @@
   @Column(id = 8, length = 40, notNull = false)
   protected String parentUuid;
 
+  @Column(id = 9, notNull = false)
+  protected CommentRange range;
+
   protected PatchLineComment() {
   }
 
-  public PatchLineComment(final PatchLineComment.Key id, final int line,
-      final Account.Id a, String parentUuid) {
+  public PatchLineComment(PatchLineComment.Key id, int line, Account.Id a,
+      String parentUuid, Timestamp when) {
     key = id;
     lineNbr = line;
     author = a;
     this.parentUuid = parentUuid;
     setStatus(Status.DRAFT);
-    updated();
+    updated(when);
   }
 
   public PatchLineComment.Key getKey() {
@@ -174,8 +177,8 @@
     message = s;
   }
 
-  public void updated() {
-    writtenOn = new Timestamp(System.currentTimeMillis());
+  public void updated(Timestamp when) {
+    writtenOn = when;
   }
 
   public void setWrittenOn(Timestamp ts) {
@@ -189,4 +192,12 @@
   public void setParentUuid(String inReplyTo) {
     parentUuid = inReplyTo;
   }
+
+  public void setRange(CommentRange r) {
+    range = r;
+  }
+
+  public CommentRange getRange() {
+    return range;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index 8d79d66..2c0f1f4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -140,11 +140,11 @@
   protected PatchSetApproval() {
   }
 
-  public PatchSetApproval(final PatchSetApproval.Key k, final short v) {
+  public PatchSetApproval(PatchSetApproval.Key k, short v, Timestamp ts) {
     key = k;
     changeOpen = true;
     setValue(v);
-    setGranted();
+    setGranted(ts);
   }
 
   public PatchSetApproval(final PatchSet.Id psId, final PatchSetApproval src) {
@@ -183,10 +183,6 @@
     return granted;
   }
 
-  public void setGranted() {
-    granted = new Timestamp(System.currentTimeMillis());
-  }
-
   public void setGranted(Timestamp ts) {
     granted = ts;
   }
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 d243496..f3cf471 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
@@ -107,6 +107,8 @@
 
   protected InheritableBoolean requireChangeID;
 
+  protected String maxObjectSizeLimit;
+
   protected InheritableBoolean useContentMerge;
 
   protected String defaultDashboardId;
@@ -160,6 +162,10 @@
     return requireChangeID;
   }
 
+  public String getMaxObjectSizeLimit() {
+    return maxObjectSizeLimit;
+  }
+
   public void setUseContributorAgreements(final InheritableBoolean u) {
     useContributorAgreements = u;
   }
@@ -176,6 +182,10 @@
     requireChangeID = cid;
   }
 
+  public void setMaxObjectSizeLimit(final String limit) {
+    maxObjectSizeLimit = limit;
+  }
+
   public SubmitType getSubmitType() {
     return submitType;
   }
@@ -224,6 +234,7 @@
     requireChangeID = update.requireChangeID;
     submitType = update.submitType;
     state = update.state;
+    maxObjectSizeLimit = update.maxObjectSizeLimit;
   }
 
   /**
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
index d90a81c..c3a535b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
@@ -54,4 +54,14 @@
     revEnd.append('z');
     return new RevId(revEnd.toString());
   }
+
+  @Override
+  public int hashCode() {
+    return id.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return (o instanceof RevId) && id.equals(((RevId) o).id);
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
new file mode 100644
index 0000000..d1eaed8
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
+
+public interface AccountGroupByIdAccess extends
+    Access<AccountGroupById, AccountGroupById.Key> {
+  @PrimaryKey("key")
+  AccountGroupById get(AccountGroupById.Key key) throws OrmException;
+
+  @Query("WHERE key.includeUUID = ?")
+  ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException;
+
+  @Query("WHERE key.groupId = ?")
+  ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException;
+
+  @Query("")
+  ResultSet<AccountGroupById> all() throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
new file mode 100644
index 0000000..e772c8c
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
+
+public interface AccountGroupByIdAudAccess extends
+    Access<AccountGroupByIdAud, AccountGroupByIdAud.Key> {
+  @PrimaryKey("key")
+  AccountGroupByIdAud get(AccountGroupByIdAud.Key key)
+      throws OrmException;
+
+  @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
+  ResultSet<AccountGroupByIdAud> byGroupInclude(AccountGroup.Id groupId,
+      AccountGroup.UUID incGroupUUID) throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
deleted file mode 100644
index 50e23eb..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupIncludeByUuidAccess extends
-    Access<AccountGroupIncludeByUuid, AccountGroupIncludeByUuid.Key> {
-  @PrimaryKey("key")
-  AccountGroupIncludeByUuid get(AccountGroupIncludeByUuid.Key key) throws OrmException;
-
-  @Query("WHERE key.includeUUID = ?")
-  ResultSet<AccountGroupIncludeByUuid> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException;
-
-  @Query("WHERE key.groupId = ?")
-  ResultSet<AccountGroupIncludeByUuid> byGroup(AccountGroup.Id id) throws OrmException;
-
-  @Query("")
-  ResultSet<AccountGroupIncludeByUuid> all() throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
deleted file mode 100644
index 1c95f75..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupIncludeByUuidAuditAccess extends
-    Access<AccountGroupIncludeByUuidAudit, AccountGroupIncludeByUuidAudit.Key> {
-  @PrimaryKey("key")
-  AccountGroupIncludeByUuidAudit get(AccountGroupIncludeByUuidAudit.Key key)
-      throws OrmException;
-
-  @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
-  ResultSet<AccountGroupIncludeByUuidAudit> byGroupInclude(AccountGroup.Id groupId,
-      AccountGroup.UUID incGroupUUID) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
index 66df78a..9618bc3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
@@ -42,12 +42,11 @@
   @Query("WHERE dest.projectName = ?")
   ResultSet<Change> byProject(Project.NameKey p) throws OrmException;
 
+  @Deprecated
   @Query("WHERE owner = ? AND open = true ORDER BY createdOn, changeId")
   ResultSet<Change> byOwnerOpen(Account.Id id) throws OrmException;
 
-  @Query("WHERE owner = ? AND open = false ORDER BY lastUpdatedOn DESC LIMIT 5")
-  ResultSet<Change> byOwnerClosed(Account.Id id) throws OrmException;
-
+  @Deprecated
   @Query("WHERE owner = ? AND open = false ORDER BY lastUpdatedOn")
   ResultSet<Change> byOwnerClosedAll(Account.Id id) throws OrmException;
 
@@ -58,9 +57,11 @@
   @Query("WHERE status = '" + Change.STATUS_SUBMITTED + "'")
   ResultSet<Change> allSubmitted() throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = true AND sortKey > ? ORDER BY sortKey LIMIT ?")
   ResultSet<Change> allOpenPrev(String sortKey, int limit) throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = true AND sortKey < ? ORDER BY sortKey DESC LIMIT ?")
   ResultSet<Change> allOpenNext(String sortKey, int limit) throws OrmException;
 
@@ -70,6 +71,7 @@
   @Query("WHERE open = true AND dest = ?")
   ResultSet<Change> byBranchOpenAll(Branch.NameKey p) throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = true AND dest.projectName = ? AND sortKey > ?"
       + " ORDER BY sortKey LIMIT ?")
   ResultSet<Change> byProjectOpenPrev(Project.NameKey p, String sortKey,
@@ -80,29 +82,35 @@
   ResultSet<Change> byProjectOpenNext(Project.NameKey p, String sortKey,
       int limit) throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = false AND status = ? AND dest.projectName = ? AND sortKey > ?"
       + " ORDER BY sortKey LIMIT ?")
   ResultSet<Change> byProjectClosedPrev(char status, Project.NameKey p,
       String sortKey, int limit) throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = false AND status = ? AND dest.projectName = ? AND sortKey < ?"
       + " ORDER BY sortKey DESC LIMIT ?")
   ResultSet<Change> byProjectClosedNext(char status, Project.NameKey p,
       String sortKey, int limit) throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = false AND status = ? AND sortKey > ? ORDER BY sortKey LIMIT ?")
   ResultSet<Change> allClosedPrev(char status, String sortKey, int limit)
       throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = false AND status = ? AND sortKey < ? ORDER BY sortKey DESC LIMIT ?")
   ResultSet<Change> allClosedNext(char status, String sortKey, int limit)
       throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = false AND status = ? AND dest = ? AND sortKey > ?"
       + " ORDER BY sortKey LIMIT ?")
   ResultSet<Change> byBranchClosedPrev(char status, Branch.NameKey p,
       String sortKey, int limit) throws OrmException;
 
+  @Deprecated
   @Query("WHERE open = false AND status = ? AND dest = ? AND sortKey < ?"
       + " ORDER BY sortKey DESC LIMIT ?")
   ResultSet<Change> byBranchClosedNext(char status, Branch.NameKey p,
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index 7e0b90c..703edbb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -30,10 +30,10 @@
   @Query("WHERE id.changeId = ? ORDER BY id.patchSetId")
   ResultSet<PatchSet> byChange(Change.Id id) throws OrmException;
 
-  @Query("WHERE revision = ? LIMIT 2")
+  @Query("WHERE revision = ?")
   ResultSet<PatchSet> byRevision(RevId rev) throws OrmException;
 
-  @Query("WHERE revision >= ? AND revision <= ? LIMIT 2")
+  @Query("WHERE revision >= ? AND revision <= ?")
   ResultSet<PatchSet> byRevisionRange(RevId reva, RevId revb)
       throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index f2b1cb7..ac2c849 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.server;
 
+import com.google.gerrit.reviewdb.client.Change.Id;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -31,6 +32,9 @@
   @Query("WHERE key.patchSetId = ? ORDER BY key.position")
   ResultSet<PatchSetAncestor> ancestorsOf(PatchSet.Id id) throws OrmException;
 
+  @Query("WHERE key.patchSetId.changeId = ?")
+  ResultSet<PatchSetAncestor> byChange(Id id) throws OrmException;
+
   @Query("WHERE key.patchSetId = ?")
   ResultSet<PatchSetAncestor> byPatchSet(PatchSet.Id id) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
index dae8e6d..25dfdbd 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
@@ -39,15 +39,12 @@
   ResultSet<PatchSetApproval> byPatchSetUser(PatchSet.Id patchSet,
       Account.Id account) throws OrmException;
 
+  @Deprecated
   @Query("WHERE changeOpen = true AND key.accountId = ?")
   ResultSet<PatchSetApproval> openByUser(Account.Id account)
       throws OrmException;
 
-  @Query("WHERE changeOpen = false AND key.accountId = ?"
-      + " ORDER BY changeSortKey DESC LIMIT 10")
-  ResultSet<PatchSetApproval> closedByUser(Account.Id account)
-      throws OrmException;
-
+  @Deprecated
   @Query("WHERE changeOpen = false AND key.accountId = ? ORDER BY changeSortKey")
   ResultSet<PatchSetApproval> closedByUserAll(Account.Id account)
       throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index f1ab752..1a6f817 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -105,10 +105,10 @@
   SubmoduleSubscriptionAccess submoduleSubscriptions();
 
   @Relation(id = 29)
-  AccountGroupIncludeByUuidAccess accountGroupIncludesByUuid();
+  AccountGroupByIdAccess accountGroupById();
 
   @Relation(id = 30)
-  AccountGroupIncludeByUuidAuditAccess accountGroupIncludesByUuidAudit();
+  AccountGroupByIdAudAccess accountGroupByIdAud();
 
   /** Create the next unique id for an {@link Account}. */
   @Sequence(startWith = 1000000)
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
index c0e1eb6..6dce287 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
@@ -36,7 +36,7 @@
    * Fetches all <code>SubmoduleSubscription</code>s in which some branch of
    * <code>superProject</code> subscribes a branch.
    *
-   * Use {@link #bySuperproject(Branch.NameKey)} to fetch for a branch instead
+   * Use {@link #bySuperProject(Branch.NameKey)} to fetch for a branch instead
    * of a project.
    *
    * @param superProject the project to fetch subscriptions for
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
index d8b2cee..9f2bae5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
@@ -29,6 +29,7 @@
   @Query("WHERE key.changeId = ?")
   ResultSet<TrackingId> byChange(Change.Id change) throws OrmException;
 
+  @Deprecated
   @Query("WHERE key.trackingKey = ?")
   ResultSet<TrackingId> byTrackingId(TrackingId.Id trackingId)
       throws OrmException;
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index d6609e7..495f7b1 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -34,17 +34,16 @@
 
 
 -- *********************************************************************
--- AccountGroupIncludeByUuidAccess
+-- AccountGroupByIdAccess
 --    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_by_uuid_byInclude
-ON account_group_includes_by_uuid (include_uuid);
-
+CREATE INDEX account_group_id_byInclude
+ON account_group_by_id (include_uuid);
 
 -- *********************************************************************
 -- AccountProjectWatchAccess
 --    @PrimaryKey covers: byAccount
 --    covers:             byProject
-CREATE INDEX account_project_watches_byProject
+CREATE INDEX account_project_watches_byP
 ON account_project_watches (project_name);
 
 
@@ -114,7 +113,7 @@
 ON patch_set_approvals (change_open, account_id);
 
 --    covers:             closedByUser
-CREATE INDEX patch_set_approvals_closedByUser
+CREATE INDEX patch_set_approvals_closedByU
 ON patch_set_approvals (change_open, account_id, change_sort_key);
 
 
@@ -163,5 +162,5 @@
 -- *********************************************************************
 -- SubmoduleSubscriptionAccess
 
-CREATE INDEX submodule_subscription_access_bySubscription
+CREATE INDEX submodule_subscr_acc_byS
 ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 3b62e84..5b8c463 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -82,17 +82,16 @@
 
 
 -- *********************************************************************
--- AccountGroupIncludeByUuidAccess
+-- AccountGroupByIdAccess
 --    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_by_uuid_byInclude
-ON account_group_includes_by_uuid (include_uuid);
-
+CREATE INDEX account_group_id_byInclude
+ON account_group_by_id (include_uuid);
 
 -- *********************************************************************
 -- AccountProjectWatchAccess
 --    @PrimaryKey covers: byAccount
 --    covers:             byProject
-CREATE INDEX account_project_watches_byProject
+CREATE INDEX account_project_watches_byP
 ON account_project_watches (project_name);
 
 
@@ -170,7 +169,7 @@
 WHERE change_open = 'Y';
 
 --    covers:             closedByUser
-CREATE INDEX patch_set_approvals_closedByUser
+CREATE INDEX patch_set_approvals_closedByU
 ON patch_set_approvals (account_id, change_sort_key)
 WHERE change_open = 'N';
 
@@ -222,5 +221,5 @@
 -- *********************************************************************
 -- SubmoduleSubscriptionAccess
 
-CREATE INDEX submodule_subscription_access_bySubscription
+CREATE INDEX submodule_subscr_acc_byS
 ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
diff --git a/gerrit-server/.settings/org.eclipse.core.resources.prefs b/gerrit-server/.settings/org.eclipse.core.resources.prefs
index 29abf99..1daeba9 100644
--- a/gerrit-server/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-server/.settings/org.eclipse.core.resources.prefs
@@ -3,4 +3,5 @@
 encoding//src/main/resources=UTF-8
 encoding//src/test/java=UTF-8
 encoding//src/test/resources=UTF-8
+encoding//target/generated-sources/prolog-java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
new file mode 100644
index 0000000..1db7555
--- /dev/null
+++ b/gerrit-server/BUCK
@@ -0,0 +1,144 @@
+SRCS = glob([
+  'src/main/java/**/*.java',
+  'src/test/java/com/google/gerrit/server/project/Util.java'
+])
+RESOURCES =  glob(['src/main/resources/**/*'])
+
+# TODO(sop) break up gerrit-server java_library(), its too big
+java_library2(
+  name = 'server',
+  srcs = SRCS,
+  resources = RESOURCES,
+  deps = [
+    '//gerrit-antlr:query_exception',
+    '//gerrit-antlr:query_parser',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-patch-commonsnet:commons-net',
+    '//gerrit-patch-jgit:server',
+    '//gerrit-prettify:server',
+    '//gerrit-reviewdb:server',
+    '//gerrit-util-cli:cli',
+    '//gerrit-util-ssl:ssl',
+    '//lib:args4j',
+    '//lib:automaton',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+    '//lib:jsch',
+    '//lib:juniversalchardet',
+    '//lib:mime-util',
+    '//lib:ow2-asm',
+    '//lib:ow2-asm-tree',
+    '//lib:ow2-asm-util',
+    '//lib:parboiled-core',
+    '//lib:pegdown',
+    '//lib:protobuf',
+    '//lib:velocity',
+    '//lib/antlr:java_runtime',
+    '//lib/commons:codec',
+    '//lib/commons:dbcp',
+    '//lib/commons:lang',
+    '//lib/commons:net',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+    '//lib/jgit:jgit',
+    '//lib/joda:joda-time',
+    '//lib/log:api',
+    '//lib/prolog:prolog-cafe',
+  ],
+  compile_deps = [
+    '//lib/bouncycastle:bcprov',
+    '//lib/bouncycastle:bcpg',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'server-src',
+  srcs = SRCS + RESOURCES,
+  visibility = ['PUBLIC'],
+)
+
+TESTUTIL = glob(['src/test/java/com/google/gerrit/testutil/**/*.java'])
+java_library(
+  name = 'testutil',
+  srcs = TESTUTIL,
+  deps = [
+    ':server',
+    '//gerrit-common:server',
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-lucene:lucene',
+    '//gerrit-reviewdb:client',
+    '//gerrit-reviewdb:server',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:h2',
+    '//lib:junit',
+    '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
+    '//lib/jgit:jgit',
+    '//lib/jgit:junit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+PROLOG_TEST_CASE = [
+  'src/test/java/com/google/gerrit/rules/PrologTestCase.java',
+]
+PROLOG_TESTS = glob(
+  ['src/test/java/com/google/gerrit/rules/**/*.java'],
+  excludes = PROLOG_TEST_CASE,
+)
+
+java_library(
+  name = 'prolog_test_case',
+  srcs = PROLOG_TEST_CASE,
+  deps = [
+    ':server',
+    '//lib:junit',
+    '//lib/guice:guice',
+    '//lib/prolog:prolog-cafe',
+  ],
+  export_deps = True,
+)
+
+java_test(
+  name = 'prolog_tests',
+  srcs = PROLOG_TESTS,
+  resources = glob(['src/test/resources/com/google/gerrit/rules/**/*']),
+  deps = [
+    ':prolog_test_case',
+    '//gerrit-server/src/main/prolog:common',
+  ],
+)
+
+java_test(
+  name = 'server_tests',
+  srcs = glob(
+    ['src/test/java/**/*.java'],
+    excludes = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE
+  ),
+  deps = [
+    ':server',
+    ':testutil',
+    '//gerrit-antlr:query_exception',
+    '//gerrit-antlr:query_parser',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//lib:easymock',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:junit',
+    '//lib/antlr:java_runtime',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/jgit:junit',
+    '//lib/joda:joda-time',
+    '//lib/prolog:prolog-cafe',
+  ],
+  source_under_test = [':server'],
+)
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
deleted file mode 100644
index e543634..0000000
--- a/gerrit-server/pom.xml
+++ /dev/null
@@ -1,248 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-server</artifactId>
-  <name>Gerrit Code Review - Server</name>
-
-  <description>
-    Commons server routines
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.velocity</groupId>
-      <artifactId>velocity</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit.junit</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>commons-dbcp</groupId>
-      <artifactId>commons-dbcp</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>commons-lang</groupId>
-      <artifactId>commons-lang</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>commons-net</groupId>
-      <artifactId>commons-net</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-api</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-log4j12</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>bouncycastle</groupId>
-      <artifactId>bcpg-jdk15</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>eu.medsea.mimeutil</groupId>
-      <artifactId>mime-util</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.inject</groupId>
-      <artifactId>guice</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.inject.extensions</groupId>
-      <artifactId>guice-servlet</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.inject.extensions</groupId>
-      <artifactId>guice-assistedinject</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>aopalliance</groupId>
-      <artifactId>aopalliance</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-antlr</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-common</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-extension-api</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-util-cli</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-util-ssl</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-patch-commonsnet</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.h2database</groupId>
-      <artifactId>h2</artifactId>
-      <scope>test</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.code.findbugs</groupId>
-      <artifactId>jsr305</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.googlecode.juniversalchardet</groupId>
-      <artifactId>juniversalchardet</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>dk.brics.automaton</groupId>
-      <artifactId>automaton</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.googlecode.prolog-cafe</groupId>
-      <artifactId>PrologCafe</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.pegdown</groupId>
-      <artifactId>pegdown</artifactId>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-antrun-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>prolog-to-java</id>
-            <phase>generate-sources</phase>
-            <goals>
-              <goal>run</goal>
-            </goals>
-            <configuration>
-              <target>
-                <property name="gensrc" location="${project.build.directory}/generated-sources"/>
-
-                <java classname="com.googlecode.prolog_cafe.compiler.Compiler"
-                    fork="true"
-                    failonerror="true"
-                    classpathref="maven.compile.classpath">
-                  <arg value="--show-stack-trace"/>
-                  <arg value="-O"/>
-                  <arg value="-am"/><arg value="${gensrc}/prolog-am"/>
-                  <arg value="-s" /><arg value="${gensrc}/prolog-java"/>
-                  <arg value="src/main/prolog/gerrit_common.pl"/>
-                </java>
-              </target>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>build-helper-maven-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>add-source</id>
-            <phase>generate-sources</phase>
-            <goals>
-              <goal>add-source</goal>
-            </goals>
-            <configuration>
-              <sources>
-                <source>${project.build.directory}/generated-sources/prolog-java</source>
-              </sources>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
index 173ced6..cdb24e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.TimeUtil;
 
 public class AuditEvent {
 
@@ -94,7 +95,7 @@
     this.params = Objects.firstNonNull(params, EMPTY_PARAMS);
     this.uuid = new UUID();
     this.result = result;
-    this.elapsed = System.currentTimeMillis() - timeAtStart;
+    this.elapsed = TimeUtil.nowMs() - timeAtStart;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 2d54601..1798f5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -44,6 +45,7 @@
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
+import com.google.gerrit.server.events.TopicChangedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.project.ProjectCache;
@@ -186,6 +188,9 @@
     /** Filename of the reviewer added hook. */
     private final File reviewerAddedHook;
 
+    /** Filename of the topic changed hook. */
+    private final File topicChangedHook;
+
     /** Filename of the cla signed hook. */
     private final File claSignedHook;
 
@@ -209,7 +214,7 @@
     private final SitePaths sitePaths;
 
     /** Thread pool used to monitor sync hooks */
-    private final ExecutorService syncHookThreadPool = Executors.newCachedThreadPool();
+    private final ExecutorService syncHookThreadPool;
 
     /** Timeout value for synchronous hooks */
     private final int syncHookTimeout;
@@ -254,9 +259,14 @@
         changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath());
         refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath());
         reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath());
+        topicChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "topicChangedHook", "topic-changed")).getPath());
         claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
         refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath());
         syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
+        syncHookThreadPool = Executors.newCachedThreadPool(
+            new ThreadFactoryBuilder()
+              .setNameFormat("SyncHook-%d")
+              .build());
     }
 
     public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
@@ -469,11 +479,13 @@
     }
 
     public void doChangeAbandonedHook(final Change change, final Account account,
-          final String reason, final ReviewDb db) throws OrmException {
+          final PatchSet patchSet, final String reason, final ReviewDb db)
+          throws OrmException {
         final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
         event.abandoner = eventFactory.asAccountAttribute(account);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         event.reason = reason;
         fireEvent(change, event, db);
 
@@ -484,17 +496,20 @@
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
         addArg(args, "--abandoner", getDisplayName(account));
+        addArg(args, "--commit", event.patchSet.revision);
         addArg(args, "--reason", reason == null ? "" : reason);
 
         runHook(change.getProject(), changeAbandonedHook, args);
     }
 
     public void doChangeRestoredHook(final Change change, final Account account,
-          final String reason, final ReviewDb db) throws OrmException {
+          final PatchSet patchSet, final String reason, final ReviewDb db)
+          throws OrmException {
         final ChangeRestoredEvent event = new ChangeRestoredEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
         event.restorer = eventFactory.asAccountAttribute(account);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         event.reason = reason;
         fireEvent(change, event, db);
 
@@ -505,6 +520,7 @@
         addArg(args, "--branch", event.change.branch);
         addArg(args, "--topic", event.change.topic);
         addArg(args, "--restorer", getDisplayName(account));
+        addArg(args, "--commit", event.patchSet.revision);
         addArg(args, "--reason", reason == null ? "" : reason);
 
         runHook(change.getProject(), changeRestoredHook, args);
@@ -554,6 +570,27 @@
       runHook(change.getProject(), reviewerAddedHook, args);
     }
 
+    public void doTopicChangedHook(final Change change, final Account account,
+        final String oldTopic, final ReviewDb db)
+            throws OrmException {
+      final TopicChangedEvent event = new TopicChangedEvent();
+
+      event.change = eventFactory.asChangeAttribute(change);
+      event.changer = eventFactory.asAccountAttribute(account);
+      event.oldTopic = oldTopic;
+      fireEvent(change, event, db);
+
+      final List<String> args = new ArrayList<String>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--changer", getDisplayName(account));
+      addArg(args, "--old-topic", oldTopic);
+      addArg(args, "--new-topic", event.change.topic);
+
+      runHook(change.getProject(), topicChangedHook, args);
+    }
+
     public void doClaSignupHook(Account account, ContributorAgreement cla) {
       if (account != null) {
         final List<String> args = new ArrayList<String>();
@@ -565,6 +602,18 @@
       }
     }
 
+    @Override
+    public void postEvent(final Change change, final ChangeEvent event,
+        final ReviewDb db) throws OrmException {
+      fireEvent(change, event, db);
+    }
+
+    @Override
+    public void postEvent(final Branch.NameKey branchName,
+        final ChangeEvent event) {
+      fireEvent(branchName, event);
+    }
+
     private void fireEventForUnrestrictedListeners(final ChangeEvent event) {
       for (ChangeListener listener : unrestrictedListeners) {
           listener.onChangeEvent(event);
@@ -776,23 +825,25 @@
         }
       }
 
-      final int exitValue = result.getExitValue();
-      if (exitValue == 0) {
-        log.debug("hook[" + getName() + "] exitValue:" + exitValue);
-      } else {
-        log.info("hook[" + getName() + "] exitValue:" + exitValue);
-      }
-
-      BufferedReader br =
-          new BufferedReader(new StringReader(result.getOutput()));
-      try {
-        String line;
-        while ((line = br.readLine()) != null) {
-          log.info("hook[" + getName() + "] output: " + line);
+      if (result != null) {
+        final int exitValue = result.getExitValue();
+        if (exitValue == 0) {
+          log.debug("hook[" + getName() + "] exitValue:" + exitValue);
+        } else {
+          log.info("hook[" + getName() + "] exitValue:" + exitValue);
         }
-      }
-      catch(IOException  iox) {
-        log.error("Error writing hook output", iox);
+
+        BufferedReader br =
+            new BufferedReader(new StringReader(result.getOutput()));
+        try {
+          String line;
+          while ((line = br.readLine()) != null) {
+            log.info("hook[" + getName() + "] output: " + line);
+          }
+        }
+        catch(IOException  iox) {
+          log.error("Error writing hook output", iox);
+        }
       }
 
       return result;
@@ -838,7 +889,7 @@
     }
   }
 
-  /** Runable type used to run async hooks */
+  /** Runnable type used to run asynchronous hooks */
   private final class AsyncHookTask extends HookTask implements Runnable {
 
     private AsyncHookTask(Project.NameKey project, File hook, List<String> args) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 48a52a0..3399272 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -103,7 +104,7 @@
    * @throws OrmException
    */
   public void doChangeAbandonedHook(Change change, Account account,
-      String reason, ReviewDb db) throws OrmException;
+      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
 
   /**
    * Fire the Change Restored Hook.
@@ -114,7 +115,7 @@
    * @throws OrmException
    */
   public void doChangeRestoredHook(Change change, Account account,
-      String reason, ReviewDb db) throws OrmException;
+      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
 
   /**
    * Fire the Ref Updated Hook
@@ -147,6 +148,16 @@
   public void doReviewerAddedHook(Change change, Account account,
       PatchSet patchSet, ReviewDb db) throws OrmException;
 
+  /**
+   * Fire the Topic Changed Hook
+   *
+   * @param change The change itself.
+   * @param account The gerrit user who changed the topic.
+   * @param oldTopic The old topic name.
+   */
+  public void doTopicChangedHook(Change change, Account account,
+      String oldTopic, ReviewDb db) throws OrmException;
+
   public void doClaSignupHook(Account account, ContributorAgreement cla);
 
   /**
@@ -157,8 +168,26 @@
    * @param uploader The gerrit user running the command
    * @param oldId The ref's old id
    * @param newId The ref's new id
-   * @param account The gerrit user who moved the ref
    */
   public HookResult doRefUpdateHook(Project project,  String refName,
        Account uploader, ObjectId oldId, ObjectId newId);
+
+  /**
+   * Post a stream event that is related to a change
+   *
+   * @param change The change that the event is related to
+   * @param event The event to post
+   * @param db The database
+   * @throws OrmException
+   */
+  public void postEvent(Change change, ChangeEvent event, ReviewDb db)
+      throws OrmException;
+
+  /**
+   * Post a stream event that is related to a branch
+   *
+   * @param branchName The branch that the event is related to
+   * @param event The event to post
+   */
+  public void postEvent(Branch.NameKey branchName, ChangeEvent event);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 6011ab0..dd68296 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -18,11 +18,13 @@
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch.NameKey;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.ChangeEvent;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -37,7 +39,7 @@
 
   @Override
   public void doChangeAbandonedHook(Change change, Account account,
-      String reason, ReviewDb db) {
+      PatchSet patchSet, String reason, ReviewDb db) {
   }
 
   @Override
@@ -52,7 +54,7 @@
 
   @Override
   public void doChangeRestoredHook(Change change, Account account,
-      String reason, ReviewDb db) {
+      PatchSet patchSet, String reason, ReviewDb db) {
   }
 
   @Override
@@ -91,6 +93,11 @@
   }
 
   @Override
+  public void doTopicChangedHook(Change change, Account account, String oldTopic,
+      ReviewDb db) {
+  }
+
+  @Override
   public void removeChangeListener(ChangeListener listener) {
   }
 
@@ -99,4 +106,12 @@
       Account uploader, ObjectId oldId, ObjectId newId) {
     return null;
   }
+
+  @Override
+  public void postEvent(Change change, ChangeEvent event, ReviewDb db) {
+  }
+
+  @Override
+  public void postEvent(Branch.NameKey branchName, ChangeEvent event) {
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
index f9aac59..a0196fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -222,7 +223,7 @@
   /** Takes compiled prolog .class files, puts them into the jar file. */
   private void createJar(File archiveFile, List<String> toBeJared,
       File tempDir, ObjectId metaConfig, ObjectId rulesId) throws IOException {
-    long now = System.currentTimeMillis();
+    long now = TimeUtil.nowMs();
     File tmpjar = File.createTempFile(".rulec_", ".jar", archiveFile.getParentFile());
     try {
       Manifest mf = new Manifest();
@@ -315,4 +316,4 @@
     }
     dir.delete();
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
index 310b401..234a0e7b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -14,8 +14,15 @@
 
 package com.google.gerrit.rules;
 
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 
 import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
@@ -56,23 +63,22 @@
     PrologEnvironment create(PrologMachineCopy src);
   }
 
-  private final Injector injector;
+  private final Args args;
   private final Map<StoredValue<Object>, Object> storedValues;
   private List<Runnable> cleanup;
 
   @Inject
-  PrologEnvironment(Injector i, @Assisted PrologMachineCopy src) {
+  PrologEnvironment(Args a, @Assisted PrologMachineCopy src) {
     super(src);
-    injector = i;
     setMaxArity(MAX_ARITY);
     setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
+    args = a;
     storedValues = new HashMap<StoredValue<Object>, Object>();
     cleanup = new LinkedList<Runnable>();
   }
 
-  /** Get the global Guice Injector that configured the environment. */
-  public Injector getInjector() {
-    return injector;
+  public Args getArgs() {
+    return args;
   }
 
   /**
@@ -139,4 +145,53 @@
       i.remove();
     }
   }
-}
\ No newline at end of file
+
+  @Singleton
+  public static class Args {
+    private final ProjectCache projectCache;
+    private final GitRepositoryManager repositoryManager;
+    private final PatchListCache patchListCache;
+    private final PatchSetInfoFactory patchSetInfoFactory;
+    private final IdentifiedUser.GenericFactory userFactory;
+    private final Provider<AnonymousUser> anonymousUser;
+
+    @Inject
+    Args(ProjectCache projectCache,
+        GitRepositoryManager repositoryManager,
+        PatchListCache patchListCache,
+        PatchSetInfoFactory patchSetInfoFactory,
+        IdentifiedUser.GenericFactory userFactory,
+        Provider<AnonymousUser> anonymousUser) {
+      this.projectCache = projectCache;
+      this.repositoryManager = repositoryManager;
+      this.patchListCache = patchListCache;
+      this.patchSetInfoFactory = patchSetInfoFactory;
+      this.userFactory = userFactory;
+      this.anonymousUser = anonymousUser;
+    }
+
+    public ProjectCache getProjectCache() {
+      return projectCache;
+    }
+
+    public GitRepositoryManager getGitRepositoryManager() {
+      return repositoryManager;
+    }
+
+    public PatchListCache getPatchListCache() {
+      return patchListCache;
+    }
+
+    public PatchSetInfoFactory getPatchSetInfoFactory() {
+      return patchSetInfoFactory;
+    }
+
+    public IdentifiedUser.GenericFactory getUserFactory() {
+      return userFactory;
+    }
+
+    public AnonymousUser getAnonymousUser() {
+      return anonymousUser.get();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
index 92b8b1b..74a3928 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
@@ -20,7 +20,15 @@
 public class PrologModule extends FactoryModule {
   @Override
   protected void configure() {
-    DynamicSet.setOf(binder(), PredicateProvider.class);
-    factory(PrologEnvironment.Factory.class);
+    install(new EnvironmentModule());
+    bind(PrologEnvironment.Args.class);
+  }
+
+  static class EnvironmentModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      DynamicSet.setOf(binder(), PredicateProvider.class);
+      factory(PrologEnvironment.Factory.class);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 1185fd3..2dbd33b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -60,7 +60,7 @@
       PatchSet ps = StoredValues.PATCH_SET.get(engine);
       PrologEnvironment env = (PrologEnvironment) engine.control;
       PatchSetInfoFactory patchInfoFactory =
-          env.getInjector().getInstance(PatchSetInfoFactory.class);
+              env.getArgs().getPatchSetInfoFactory();
       try {
         return patchInfoFactory.get(change, ps);
       } catch (PatchSetInfoNotAvailableException e) {
@@ -74,7 +74,7 @@
     public PatchList createValue(Prolog engine) {
       PrologEnvironment env = (PrologEnvironment) engine.control;
       PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
-      PatchListCache plCache = env.getInjector().getInstance(PatchListCache.class);
+      PatchListCache plCache = env.getArgs().getPatchListCache();
       Change change = StoredValues.CHANGE.get(engine);
       Project.NameKey projectKey = change.getProject();
       ObjectId a = null;
@@ -95,8 +95,7 @@
     @Override
     public Repository createValue(Prolog engine) {
       PrologEnvironment env = (PrologEnvironment) engine.control;
-      GitRepositoryManager gitMgr =
-        env.getInjector().getInstance(GitRepositoryManager.class);
+      GitRepositoryManager gitMgr = env.getArgs().getGitRepositoryManager();
       Change change = StoredValues.CHANGE.get(engine);
       Project.NameKey projectKey = change.getProject();
       final Repository repo;
@@ -122,7 +121,7 @@
         @Override
         protected AnonymousUser createValue(Prolog engine) {
           PrologEnvironment env = (PrologEnvironment) engine.control;
-          return env.getInjector().getInstance(AnonymousUser.class);
+          return env.getArgs().getAnonymousUser();
         }
       };
 
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 3fd24f1..deb6f30 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
@@ -26,6 +26,8 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.PatchSetInserter.ChangeKind;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -69,10 +71,10 @@
    * @throws OrmException
    */
   public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
-      PatchSet.Id source, PatchSet.Id dest) throws OrmException {
+      PatchSet.Id source, PatchSet dest, ChangeKind changeKind) throws OrmException {
     Iterable<PatchSetApproval> sourceApprovals =
         db.patchSetApprovals().byPatchSet(source);
-    copyLabels(db, labelTypes, sourceApprovals, source, dest);
+    copyLabels(db, labelTypes, sourceApprovals, source, dest, changeKind);
   }
 
   /**
@@ -82,7 +84,7 @@
    */
   public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
       Iterable<PatchSetApproval> sourceApprovals, PatchSet.Id source,
-      PatchSet.Id dest) throws OrmException {
+      PatchSet dest, ChangeKind changeKind) throws OrmException {
     List<PatchSetApproval> copied = Lists.newArrayList();
     for (PatchSetApproval a : sourceApprovals) {
       if (source.equals(a.getPatchSetId())) {
@@ -90,9 +92,15 @@
         if (type == null) {
           continue;
         } else if (type.isCopyMinScore() && type.isMaxNegative(a)) {
-          copied.add(new PatchSetApproval(dest, a));
+          copied.add(new PatchSetApproval(dest.getId(), a));
         } else if (type.isCopyMaxScore() && type.isMaxPositive(a)) {
-          copied.add(new PatchSetApproval(dest, a));
+          copied.add(new PatchSetApproval(dest.getId(), a));
+        } else if (type.isCopyAllScoresOnTrivialRebase()
+            && ChangeKind.TRIVIAL_REBASE.equals(changeKind)) {
+          copied.add(new PatchSetApproval(dest.getId(), a));
+        } else if (type.isCopyAllScoresIfNoCodeChange()
+            && ChangeKind.NO_CODE_CHANGE.equals(changeKind)) {
+          copied.add(new PatchSetApproval(dest.getId(), a));
         }
       }
     }
@@ -129,7 +137,7 @@
     for (Account.Id account : need) {
       PatchSetApproval psa = new PatchSetApproval(
           new PatchSetApproval.Key(ps.getId(), account, labelId),
-          (short) 0);
+          (short) 0, TimeUtil.nowTs());
       psa.cache(change);
       cells.add(psa);
     }
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 8f73014..24a4b16 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -14,39 +14,43 @@
 
 package com.google.gerrit.server;
 
+import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeMessages;
+import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeOp;
 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.CommitMessageEditedSender;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 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.RefControl;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.MagicBranch;
-import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 
@@ -57,6 +61,7 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -64,6 +69,8 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.ChangeIdUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -78,11 +85,23 @@
 import java.util.regex.Matcher;
 
 public class ChangeUtil {
+  /**
+   * Epoch for sort key calculations, Tue Sep 30 2008 17:00:00.
+   * <p>
+   * We overrun approximately 4,083 years later, so ~6092.
+   */
+  @VisibleForTesting
+  public static final long SORT_KEY_EPOCH_MINS =
+      MINUTES.convert(1222819200L, SECONDS);
+
   private static final Object uuidLock = new Object();
   private static final int SEED = 0x2418e6f9;
   private static int uuidPrefix;
   private static int uuidSeq;
 
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeUtil.class);
+
   /**
    * Generate a new unique identifier for change message entities.
    *
@@ -116,8 +135,17 @@
     }
   }
 
+  public static void bumpRowVersionNotLastUpdatedOn(Change.Id id, ReviewDb db)
+      throws OrmException {
+    // Empty update of Change to bump rowVersion, changing its ETag.
+    Change c = db.changes().get(id);
+    if (c != null) {
+      db.changes().update(Collections.singleton(c));
+    }
+  }
+
   public static void updated(final Change c) {
-    c.resetLastUpdatedOn();
+    c.setLastUpdatedOn(TimeUtil.nowTs());
     computeSortKey(c);
   }
 
@@ -174,11 +202,6 @@
     db.trackingIds().delete(toDelete);
   }
 
-  public static void testMerge(MergeOp.Factory opFactory, Change change)
-      throws NoSuchProjectException {
-    opFactory.create(change.getDest()).verifyMergeability(change);
-  }
-
   public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
       throws OrmException {
     final int cnt = src.getParentCount();
@@ -197,7 +220,7 @@
       ReviewDb db, RevertedSender.Factory revertedSenderFactory,
       ChangeHooks hooks, Repository git,
       PatchSetInfoFactory patchSetInfoFactory, PersonIdent myIdent,
-      ChangeInserter changeInserter)
+      ChangeInserter.Factory changeInserterFactory)
           throws NoSuchChangeException, EmailException,
       OrmException, MissingObjectException, IncorrectObjectTypeException,
       IOException, InvalidChangeOperationException {
@@ -223,7 +246,7 @@
       revertCommitBuilder.addParentId(commitToRevert);
       revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
       revertCommitBuilder.setAuthor(authorIdent);
-      revertCommitBuilder.setCommitter(myIdent);
+      revertCommitBuilder.setCommitter(authorIdent);
 
       if (message == null) {
         message = MessageFormat.format(
@@ -250,20 +273,17 @@
           new Change.Key("I" + computedChangeId.name()),
           new Change.Id(db.nextChangeId()),
           user.getAccountId(),
-          changeToRevert.getDest());
+          changeToRevert.getDest(),
+          TimeUtil.nowTs());
       change.setTopic(changeToRevert.getTopic());
-
-      PatchSet.Id id =
-          new PatchSet.Id(change.getId(), Change.INITIAL_PATCH_SET_ID);
-      final PatchSet ps = new PatchSet(id);
-      ps.setCreatedOn(change.getCreatedOn());
-      ps.setUploader(change.getOwner());
-      ps.setRevision(new RevId(revertCommit.name()));
+      ChangeInserter ins =
+          changeInserterFactory.create(refControl, change, revertCommit);
+      PatchSet ps = ins.getPatchSet();
 
       String ref = refControl.getRefName();
       final String cmdRef =
           MagicBranch.NEW_PUBLISH_CHANGE
-              + ref.substring(ref.lastIndexOf("/") + 1);
+              + ref.substring(ref.lastIndexOf('/') + 1);
       CommitReceivedEvent commitReceivedEvent =
           new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
               revertCommit.getId(), cmdRef), refControl.getProjectControl()
@@ -275,11 +295,6 @@
         throw new InvalidChangeOperationException(e.getMessage());
       }
 
-      PatchSetInfo info = patchSetInfoFactory.get(revertCommit, ps.getId());
-      change.setCurrentPatchSet(info);
-      ChangeUtil.updated(change);
-
-
       final RefUpdate ru = git.updateRef(ps.getRefName());
       ru.setExpectedOldObjectId(ObjectId.zeroId());
       ru.setNewObjectId(revertCommit);
@@ -290,23 +305,26 @@
             change.getDest().getParentKey().get(), ru.getResult()));
       }
 
-      final ChangeMessage cmsg =
-          new ChangeMessage(new ChangeMessage.Key(changeId,
-              ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+      final ChangeMessage cmsg = new ChangeMessage(
+          new ChangeMessage.Key(changeId, ChangeUtil.messageUUID(db)),
+          user.getAccountId(), TimeUtil.nowTs(), patchSetId);
       final StringBuilder msgBuf =
           new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
       msgBuf.append("\n\n");
       msgBuf.append("This patchset was reverted in change: " + change.getKey().get());
       cmsg.setMessage(msgBuf.toString());
 
-      LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
-      changeInserter.insertChange(db, change, cmsg, ps, revertCommit,
-          labelTypes, info, Collections.<Account.Id> emptySet());
+      ins.setMessage(cmsg).insert();
 
-      final RevertedSender cm = revertedSenderFactory.create(change);
-      cm.setFrom(user.getAccountId());
-      cm.setChangeMessage(cmsg);
-      cm.send();
+      try {
+        final RevertedSender cm = revertedSenderFactory.create(change);
+        cm.setFrom(user.getAccountId());
+        cm.setChangeMessage(cmsg);
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot send email for revert change " + change.getId(),
+            err);
+      }
 
       return change.getId();
     } finally {
@@ -315,13 +333,11 @@
   }
 
   public static Change.Id editCommitMessage(final PatchSet.Id patchSetId,
-      final RefControl refControl, CommitValidators commitValidators,
-      final IdentifiedUser user, final String message, final ReviewDb db,
+      final RefControl refControl, final IdentifiedUser user,
+      final String message, final ReviewDb db,
       final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
-      final ChangeHooks hooks, Repository git,
-      final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
-      final TrackingFooters trackingFooters)
+      Repository git, PersonIdent myIdent,
+      PatchSetInserter.Factory patchSetInserterFactory)
       throws NoSuchChangeException, EmailException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, PatchSetInfoNotAvailableException {
@@ -332,15 +348,18 @@
     }
 
     if (message == null || message.length() == 0) {
-      throw new InvalidChangeOperationException("The commit message cannot be empty");
+      throw new InvalidChangeOperationException(
+          "The commit message cannot be empty");
     }
 
     final RevWalk revWalk = new RevWalk(git);
     try {
       RevCommit commit =
-          revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision().get()));
+          revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision()
+              .get()));
       if (commit.getFullMessage().equals(message)) {
-        throw new InvalidChangeOperationException("New commit message cannot be same as existing commit message");
+        throw new InvalidChangeOperationException(
+            "New commit message cannot be same as existing commit message");
       }
 
       Date now = myIdent.getWhen();
@@ -370,99 +389,19 @@
       newPatchSet.setCreatedOn(new Timestamp(now.getTime()));
       newPatchSet.setUploader(user.getAccountId());
       newPatchSet.setRevision(new RevId(newCommit.name()));
-      newPatchSet.setDraft(originalPS.isDraft());
 
-      final PatchSetInfo info =
-          patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+      final String msg =
+          "Patch Set " + newPatchSet.getPatchSetId()
+              + ": Commit message was updated";
 
-      final String refName = newPatchSet.getRefName();
-      CommitReceivedEvent commitReceivedEvent =
-          new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
-              newCommit.getId(), refName.substring(0,
-                  refName.lastIndexOf("/") + 1) + "new"), refControl
-              .getProjectControl().getProject(), refControl.getRefName(),
-              newCommit, user);
-
-      try {
-        commitValidators.validateForReceiveCommits(commitReceivedEvent);
-      } catch (CommitValidationException e) {
-        throw new InvalidChangeOperationException(e.getMessage());
-      }
-
-      final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-      ru.setExpectedOldObjectId(ObjectId.zeroId());
-      ru.setNewObjectId(newCommit);
-      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()));
-      }
-      gitRefUpdated.fire(change.getProject(), ru);
-
-      db.changes().beginTransaction(change.getId());
-      try {
-        Change updatedChange = db.changes().get(change.getId());
-        if (updatedChange != null && updatedChange.getStatus().isOpen()) {
-          change = updatedChange;
-        } else {
-          throw new InvalidChangeOperationException(String.format(
-              "Change %s is closed", change.getId()));
-        }
-
-        ChangeUtil.insertAncestors(db, newPatchSet.getId(), commit);
-        db.patchSets().insert(Collections.singleton(newPatchSet));
-        updatedChange =
-            db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-              @Override
-              public Change update(Change change) {
-                if (change.getStatus().isClosed()) {
-                  return null;
-                }
-                if (!change.currentPatchSetId().equals(patchSetId)) {
-                  return null;
-                }
-                if (change.getStatus() != Change.Status.DRAFT) {
-                  change.setStatus(Change.Status.NEW);
-                }
-                change.setLastSha1MergeTested(null);
-                change.setCurrentPatchSet(info);
-                ChangeUtil.updated(change);
-                return change;
-              }
-            });
-        if (updatedChange != null) {
-          change = updatedChange;
-        } else {
-          throw new InvalidChangeOperationException(String.format(
-              "Change %s was modified", change.getId()));
-        }
-
-        ApprovalsUtil.copyLabels(db,
-            refControl.getProjectControl().getLabelTypes(),
-            originalPS.getId(),
-            change.currentPatchSetId());
-
-        final List<FooterLine> footerLines = newCommit.getFooterLines();
-        updateTrackingIds(db, change, trackingFooters, footerLines);
-
-        final ChangeMessage cmsg =
-            new ChangeMessage(new ChangeMessage.Key(changeId,
-                ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
-        final String msg = "Patch Set " + newPatchSet.getPatchSetId() + ": Commit message was updated";
-        cmsg.setMessage(msg);
-        db.changeMessages().insert(Collections.singleton(cmsg));
-        db.commit();
-
-        final CommitMessageEditedSender cm = commitMessageEditedSenderFactory.create(change);
-        cm.setFrom(user.getAccountId());
-        cm.setChangeMessage(cmsg);
-        cm.send();
-      } finally {
-        db.rollback();
-      }
-
-      hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+      change = patchSetInserterFactory
+          .create(git, revWalk, refControl, user, change, newCommit)
+          .setPatchSet(newPatchSet)
+          .setMessage(msg)
+          .setCopyLabels(true)
+          .setValidatePolicy(RECEIVE_COMMITS)
+          .setDraft(originalPS.isDraft())
+          .insert();
 
       return change.getId();
     } finally {
@@ -470,12 +409,19 @@
     }
   }
 
-  public static void deleteDraftChange(final PatchSet.Id patchSetId,
+  public static void deleteDraftChange(PatchSet.Id patchSetId,
       GitRepositoryManager gitManager,
-      final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
+      GitReferenceUpdated gitRefUpdated, ReviewDb db, ChangeIndexer indexer)
       throws NoSuchChangeException, OrmException, IOException {
     final Change.Id changeId = patchSetId.getParentKey();
-    final Change change = db.changes().get(changeId);
+    deleteDraftChange(changeId, gitManager, gitRefUpdated, db, indexer);
+  }
+
+  public static void deleteDraftChange(Change.Id changeId,
+      GitRepositoryManager gitManager,
+      GitReferenceUpdated gitRefUpdated, ReviewDb db, ChangeIndexer indexer)
+      throws NoSuchChangeException, OrmException, IOException {
+    Change change = db.changes().get(changeId);
     if (change == null || change.getStatus() != Change.Status.DRAFT) {
       throw new NoSuchChangeException(changeId);
     }
@@ -489,6 +435,7 @@
     db.starredChanges().delete(db.starredChanges().byChange(changeId));
     db.trackingIds().delete(db.trackingIds().byChange(changeId));
     db.changes().delete(Collections.singleton(change));
+    indexer.delete(change);
   }
 
   public static void deleteOnlyDraftPatchSet(final PatchSet patch,
@@ -496,7 +443,7 @@
       final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
       throws NoSuchChangeException, OrmException, IOException {
     final PatchSet.Id patchSetId = patch.getId();
-    if (patch == null || !patch.isDraft()) {
+    if (!patch.isDraft()) {
       throw new NoSuchChangeException(patchSetId.getParentKey());
     }
 
@@ -530,22 +477,27 @@
     db.patchSets().delete(Collections.singleton(patch));
   }
 
-  public static String sortKey(long lastUpdated, int id){
-    // The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
-    // We overrun approximately 4,085 years later, so ~6093.
-    //
-    final long lastUpdatedOn = (lastUpdated / 1000L) - 1222819200L;
-    final StringBuilder r = new StringBuilder(16);
+  public static String sortKey(long lastUpdatedMs, int id){
+    long lastUpdatedMins = MINUTES.convert(lastUpdatedMs, MILLISECONDS);
+    long minsSinceEpoch = lastUpdatedMins - SORT_KEY_EPOCH_MINS;
+    StringBuilder r = new StringBuilder(16);
     r.setLength(16);
-    formatHexInt(r, 0, (int) (lastUpdatedOn / 60));
+    formatHexInt(r, 0, Ints.checkedCast(minsSinceEpoch));
     formatHexInt(r, 8, id);
     return r.toString();
   }
 
-  public static void computeSortKey(final Change c) {
-    long lastUpdated = c.getLastUpdatedOn().getTime();
+  public static long parseSortKey(String sortKey) {
+    if ("z".equals(sortKey)) {
+      return Long.MAX_VALUE;
+    }
+    return Long.parseLong(sortKey, 16);
+  }
+
+  public static void computeSortKey(Change c) {
+    long lastUpdatedMs = c.getLastUpdatedOn().getTime();
     int id = c.getId().get();
-    c.setSortKey(sortKey(lastUpdated, id));
+    c.setSortKey(sortKey(lastUpdatedMs, id));
   }
 
   public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
@@ -557,8 +509,9 @@
     return next;
   }
 
-  public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) {
-    return nextPatchSetId(git.getAllRefs(), id);
+  public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id)
+      throws IOException {
+    return nextPatchSetId(git.getRefDatabase().getRefs(RefDatabase.ALL), id);
   }
 
   private static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
index e64533c..0e86d34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -18,8 +18,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.args4j.AccountGroupIdHandler;
 import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.server.args4j.AccountIdHandler;
@@ -28,11 +26,12 @@
 import com.google.gerrit.server.args4j.PatchSetIdHandler;
 import com.google.gerrit.server.args4j.ProjectControlHandler;
 import com.google.gerrit.server.args4j.SocketAddressHandler;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
-
+import com.google.gerrit.util.cli.OptionHandlers;
 import org.eclipse.jgit.lib.ObjectId;
-
 import org.kohsuke.args4j.spi.OptionHandler;
 
 import java.net.SocketAddress;
@@ -44,6 +43,7 @@
   @Override
   protected void configure() {
     factory(CmdLineParser.Factory.class);
+    bind(OptionHandlers.class);
 
     registerOptionHandler(Account.Id.class, AccountIdHandler.class);
     registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
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 86a6ef8..4f2c6b9 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
@@ -51,6 +51,20 @@
   }
 
   /**
+   * Identity of the authenticated user.
+   * <p>
+   * In the normal case where a user authenticates as themselves
+   * {@code getRealUser() == this}.
+   * <p>
+   * If {@code X-Gerrit-RunAs} or {@code suexec} was used this method returns
+   * the identity of the account that has permission to act on behalf of this
+   * user.
+   */
+  public CurrentUser getRealUser() {
+    return this;
+  }
+
+  /**
    * Get the set of groups the user is currently a member of.
    * <p>
    * The returned set may be a subset of the user's actual groups; if the user's
@@ -76,11 +90,14 @@
 
   /** Capabilities available to this user account. */
   public CapabilityControl getCapabilities() {
-    CapabilityControl ctl = capabilities;
-    if (ctl == null) {
-      ctl = capabilityControlFactory.create(this);
-      capabilities = ctl;
+    if (capabilities == null) {
+      capabilities = capabilityControlFactory.create(this);
     }
-    return ctl;
+    return capabilities;
+  }
+
+  /** Check if user is the IdentifiedUser */
+  public boolean isIdentifiedUser() {
+    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 3826293..4f033d0 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
@@ -15,6 +15,8 @@
 package com.google.gerrit.server;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -34,6 +36,7 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
@@ -53,13 +56,10 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.TimeZone;
 
-import javax.annotation.Nullable;
-
 /** An authenticated user. */
 public class IdentifiedUser extends CurrentUser {
   /** Create an IdentifiedUser, ignoring any per-request state. */
@@ -97,13 +97,20 @@
     public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
-          groupBackend, null, db, id);
+          groupBackend, null, db, id, null);
     }
 
     public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
-          groupBackend, Providers.of(remotePeer), null, id);
+          groupBackend, Providers.of(remotePeer), null, id,  null);
+    }
+
+    public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
+        @Nullable CurrentUser caller) {
+      return new IdentifiedUser(capabilityControlFactory,
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupBackend, Providers.of(remotePeer), null, id, caller);
     }
   }
 
@@ -152,7 +159,13 @@
     public IdentifiedUser create(Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
-          groupBackend, remotePeerProvider, dbProvider, id);
+          groupBackend, remotePeerProvider, dbProvider, id, null);
+    }
+
+    public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
+      return new IdentifiedUser(capabilityControlFactory,
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupBackend, remotePeerProvider, dbProvider, id, caller);
     }
   }
 
@@ -182,7 +195,9 @@
   private Set<String> emailAddresses;
   private GroupMembership effectiveGroups;
   private Set<Change.Id> starredChanges;
+  private ResultSet<StarredChange> starredQuery;
   private Collection<AccountProjectWatch> notificationFilters;
+  private CurrentUser realUser;
 
   private IdentifiedUser(
       CapabilityControl.Factory capabilityControlFactory,
@@ -192,7 +207,9 @@
       final Realm realm, final AccountCache accountCache,
       final GroupBackend groupBackend,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
-      @Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
+      @Nullable final Provider<ReviewDb> dbProvider,
+      final Account.Id id,
+      @Nullable CurrentUser realUser) {
     super(capabilityControlFactory);
     this.canonicalUrl = canonicalUrl;
     this.accountCache = accountCache;
@@ -202,6 +219,12 @@
     this.remotePeerProvider = remotePeerProvider;
     this.dbProvider = dbProvider;
     this.accountId = id;
+    this.realUser = realUser != null ? realUser : this;
+  }
+
+  @Override
+  public CurrentUser getRealUser() {
+    return realUser;
   }
 
   // TODO(cranger): maybe get the state through the accountCache instead.
@@ -274,11 +297,18 @@
       if (dbProvider == null) {
         throw new OutOfScopeException("Not in request scoped user");
       }
-      final Set<Change.Id> h = new HashSet<Change.Id>();
+      Set<Change.Id> h = Sets.newHashSet();
       try {
-        for (final StarredChange sc : dbProvider.get().starredChanges()
-            .byAccount(getAccountId())) {
-          h.add(sc.getChangeId());
+        if (starredQuery != null) {
+          for (StarredChange sc : starredQuery) {
+            h.add(sc.getChangeId());
+          }
+          starredQuery = null;
+        } else {
+          for (StarredChange sc : dbProvider.get().starredChanges()
+              .byAccount(getAccountId())) {
+            h.add(sc.getChangeId());
+          }
         }
       } catch (OrmException e) {
         log.warn("Cannot query starred by user changes", e);
@@ -288,6 +318,29 @@
     return starredChanges;
   }
 
+  public void asyncStarredChanges() {
+    if (starredChanges == null && dbProvider != null) {
+      try {
+        starredQuery =
+            dbProvider.get().starredChanges().byAccount(getAccountId());
+      } catch (OrmException e) {
+        log.warn("Cannot query starred by user changes", e);
+        starredQuery = null;
+        starredChanges = Collections.emptySet();
+      }
+    }
+  }
+
+  public void abortStarredChanges() {
+    if (starredQuery != null) {
+      try {
+        starredQuery.close();
+      } finally {
+        starredQuery = null;
+      }
+    }
+  }
+
   @Override
   public Collection<AccountProjectWatch> getNotificationFilters() {
     if (notificationFilters == null) {
@@ -390,4 +443,10 @@
   public String toString() {
     return "IdentifiedUser[account " + getAccountId() + "]";
   }
+
+  /** Check if user is the IdentifiedUser */
+  @Override
+  public boolean isIdentifiedUser() {
+    return true;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java
new file mode 100644
index 0000000..58f93d8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java
@@ -0,0 +1,53 @@
+// 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.access;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class AccessCollection implements
+    RestCollection<TopLevelResource, AccessResource> {
+  private final Provider<ListAccess> list;
+  private final DynamicMap<RestView<AccessResource>> views;
+
+  @Inject
+  AccessCollection(Provider<ListAccess> list,
+      DynamicMap<RestView<AccessResource>> views) {
+    this.list = list;
+    this.views = views;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public AccessResource parse(TopLevelResource parent, IdString id)
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<AccessResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessResource.java
new file mode 100644
index 0000000..22888b8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessResource.java
@@ -0,0 +1,24 @@
+// 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.access;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class AccessResource implements RestResource {
+  public static final TypeLiteral<RestView<AccessResource>> ACCESS_KIND =
+      new TypeLiteral<RestView<AccessResource>>() {};
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
new file mode 100644
index 0000000..4002c1a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -0,0 +1,308 @@
+// 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.access;
+
+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.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.GroupJson;
+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.ProjectJson;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ListAccess implements RestReadView<TopLevelResource> {
+
+  @Option(name = "--project", aliases = {"-p"}, metaVar = "PROJECT",
+      usage = "projects for which the access rights should be returned")
+  private List<String> projects = Lists.newArrayList();
+
+  private final Provider<CurrentUser> self;
+  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ProjectCache projectCache;
+  private final ProjectJson projectJson;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupBackend groupBackend;
+  private final AllProjectsName allProjectsName;
+
+  @Inject
+  public ListAccess(Provider<CurrentUser> self,
+      ProjectControl.GenericFactory projectControlFactory,
+      ProjectCache projectCache, ProjectJson projectJson,
+      MetaDataUpdate.Server metaDataUpdateFactory,
+      GroupControl.Factory groupControlFactory, GroupBackend groupBackend,
+      GroupJson groupJson, AllProjectsName allProjectsName) {
+    this.self = self;
+    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
+    this.projectJson = projectJson;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.groupControlFactory = groupControlFactory;
+    this.groupBackend = groupBackend;
+    this.allProjectsName = allProjectsName;
+  }
+
+  @Override
+  public Map<String, ProjectAccessInfo> apply(TopLevelResource resource)
+      throws ResourceNotFoundException, ResourceConflictException, IOException {
+    Map<String, ProjectAccessInfo> access = Maps.newTreeMap();
+    for (String p: projects) {
+      Project.NameKey projectName = new Project.NameKey(p);
+      ProjectControl pc = open(projectName);
+      ProjectConfig config;
+
+      try {
+        // Load the current configuration from the repository, ensuring it's the most
+        // recent version available. If it differs from what was in the project
+        // state, force a cache flush now.
+        //
+        MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
+        try {
+          config = ProjectConfig.read(md);
+
+          if (config.updateGroupNames(groupBackend)) {
+            md.setMessage("Update group names\n");
+            config.commit(md);
+            projectCache.evict(config.getProject());
+            pc = open(projectName);
+          } else if (config.getRevision() != null
+              && !config.getRevision().equals(
+                  pc.getProjectState().getConfig().getRevision())) {
+            projectCache.evict(config.getProject());
+            pc = open(projectName);
+          }
+        } catch (ConfigInvalidException e) {
+          throw new ResourceConflictException(e.getMessage());
+        } finally {
+          md.close();
+        }
+      } catch (RepositoryNotFoundException e) {
+        throw new ResourceNotFoundException(p);
+      }
+
+      access.put(p, new ProjectAccessInfo(pc, config));
+    }
+    return access;
+  }
+
+  private ProjectControl open(Project.NameKey projectName)
+      throws ResourceNotFoundException, IOException {
+    try {
+      return projectControlFactory.validateFor(projectName,
+          ProjectControl.OWNER | ProjectControl.VISIBLE, self.get());
+    } catch (NoSuchProjectException e) {
+      throw new ResourceNotFoundException(projectName.get());
+    }
+  }
+
+  public class ProjectAccessInfo {
+    public String revision;
+    public ProjectInfo inheritsFrom;
+    public Map<String, AccessSectionInfo> local;
+    public Boolean isOwner;
+    public Set<String> ownerOf;
+    public Boolean canUpload;
+    public Boolean canAdd;
+    public Boolean configVisible;
+
+    public ProjectAccessInfo(ProjectControl pc, ProjectConfig config) {
+      final RefControl metaConfigControl =
+          pc.controlForRef(GitRepositoryManager.REF_CONFIG);
+      local = Maps.newHashMap();
+      ownerOf = Sets.newHashSet();
+      Map<AccountGroup.UUID, Boolean> visibleGroups =
+          new HashMap<AccountGroup.UUID, Boolean>();
+
+      for (AccessSection section : config.getAccessSections()) {
+        String name = section.getName();
+        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+          if (pc.isOwner()) {
+            local.put(name, new AccessSectionInfo(section));
+            ownerOf.add(name);
+
+          } else if (metaConfigControl.isVisible()) {
+            local.put(section.getName(), new AccessSectionInfo(section));
+          }
+
+        } else if (RefConfigSection.isValid(name)) {
+          RefControl rc = pc.controlForRef(name);
+          if (rc.isOwner()) {
+            local.put(name, new AccessSectionInfo(section));
+            ownerOf.add(name);
+
+          } else if (metaConfigControl.isVisible()) {
+            local.put(name, new AccessSectionInfo(section));
+
+          } else if (rc.isVisible()) {
+            // Filter the section to only add rules describing groups that
+            // are visible to the current-user. This includes any group the
+            // user is a member of, as well as groups they own or that
+            // are visible to all users.
+
+            AccessSection dst = null;
+            for (Permission srcPerm : section.getPermissions()) {
+              Permission dstPerm = null;
+
+              for (PermissionRule srcRule : srcPerm.getRules()) {
+                AccountGroup.UUID group = srcRule.getGroup().getUUID();
+                if (group == null) {
+                  continue;
+                }
+
+                Boolean canSeeGroup = visibleGroups.get(group);
+                if (canSeeGroup == null) {
+                  try {
+                    canSeeGroup = groupControlFactory.controlFor(group).isVisible();
+                  } catch (NoSuchGroupException e) {
+                    canSeeGroup = Boolean.FALSE;
+                  }
+                  visibleGroups.put(group, canSeeGroup);
+                }
+
+                if (canSeeGroup) {
+                  if (dstPerm == null) {
+                    if (dst == null) {
+                      dst = new AccessSection(name);
+                      local.put(name, new AccessSectionInfo(dst));
+                    }
+                    dstPerm = dst.getPermission(srcPerm.getName(), true);
+                  }
+                  dstPerm.add(srcRule);
+                }
+              }
+            }
+          }
+        }
+      }
+
+      if (ownerOf.isEmpty() && pc.isOwnerAnyRef()) {
+        // Special case: If the section list is empty, this project has no current
+        // access control information. Rely on what ProjectControl determines
+        // is ownership, which probably means falling back to site administrators.
+        ownerOf.add(AccessSection.ALL);
+      }
+
+
+      if (config.getRevision() != null) {
+        revision = config.getRevision().name();
+      }
+
+      ProjectState parent =
+          Iterables.getFirst(pc.getProjectState().parents(), null);
+      if (parent != null) {
+        inheritsFrom = projectJson.format(parent.getProject());
+      }
+
+      if (pc.getProject().getNameKey().equals(allProjectsName)) {
+        if (pc.isOwner()) {
+          ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
+        }
+      }
+
+      isOwner = toBoolean(pc.isOwner());
+      canUpload = toBoolean(pc.isOwner()
+          || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
+      canAdd = toBoolean(pc.canAddRefs());
+      configVisible = pc.isOwner() || metaConfigControl.isVisible();
+    }
+  }
+
+  public class AccessSectionInfo {
+    public Map<String, PermissionInfo> permissions;
+
+    public AccessSectionInfo(AccessSection section) {
+      permissions = Maps.newHashMap();
+      for (Permission p : section.getPermissions()) {
+        permissions.put(p.getName(), new PermissionInfo(p));
+      }
+    }
+  }
+
+  public class PermissionInfo {
+    public String label;
+    public Boolean exclusive;
+    public Map<String, PermissionRuleInfo> rules;
+
+    public PermissionInfo(Permission permission) {
+      label = permission.getLabel();
+      exclusive = toBoolean(permission.getExclusiveGroup());
+      rules = Maps.newHashMap();
+      for (PermissionRule r : permission.getRules()) {
+        rules.put(r.getGroup().getUUID().get(), new PermissionRuleInfo(r));
+      }
+    }
+  }
+
+  public class PermissionRuleInfo {
+    public PermissionRule.Action action;
+    public Boolean force;
+    public Integer min;
+    public Integer max;
+
+
+    public PermissionRuleInfo(PermissionRule rule) {
+      action = rule.getAction();
+      force = toBoolean(rule.getForce());
+      if (hasRange(rule)) {
+        min = rule.getMin();
+        max = rule.getMax();
+      }
+    }
+
+    private boolean hasRange(PermissionRule rule) {
+      return (!(rule.getMin() == null || rule.getMin() == 0))
+          || (!(rule.getMax() == null || rule.getMax() == 0));
+    }
+  }
+
+  private static Boolean toBoolean(boolean value) {
+    return value ? true : null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/Module.java
new file mode 100644
index 0000000..cd0d334
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/Module.java
@@ -0,0 +1,29 @@
+// 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.access;
+
+import static com.google.gerrit.server.access.AccessResource.ACCESS_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class Module extends RestApiModule {
+  @Override
+  protected void configure() {
+    bind(AccessCollection.class);
+
+    DynamicMap.mapOf(binder(), ACCESS_KIND);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index b74daa5..0e10080 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -115,7 +116,7 @@
   }
 
   private static AccountState missing(Account.Id accountId) {
-    Account account = new Account(accountId);
+    Account account = new Account(accountId, TimeUtil.nowTs());
     Collection<AccountExternalId> ids = Collections.emptySet();
     Set<AccountGroup.UUID> anon = ImmutableSet.of(AccountGroup.ANONYMOUS_USERS);
     return new AccountState(account, anon, ids);
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 f148b31..1440eac 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
@@ -96,7 +96,7 @@
    */
   public boolean canSee(final Account.Id otherUser) {
     // Special case: I can always see myself.
-    if (currentUser instanceof IdentifiedUser
+    if (currentUser.isIdentifiedUser()
         && ((IdentifiedUser) currentUser).getAccountId().equals(otherUser)) {
       return true;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
new file mode 100644
index 0000000..a4881a4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -0,0 +1,59 @@
+// 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.account;
+
+import java.util.Set;
+
+/**
+ * Directory of user account information.
+ *
+ * Implementations supply data to Gerrit about user accounts.
+ */
+public abstract class AccountDirectory {
+  /** Fields to be populated for a REST API response. */
+  public enum FillOptions {
+    /** Human friendly display name presented in the web interface. */
+    NAME,
+
+    /** Preferred email address to contact the user at. */
+    EMAIL,
+
+    /** User profile images. */
+    AVATARS,
+
+    /** Unique user identity to login to Gerrit, may be deprecated. */
+    USERNAME;
+  }
+
+  public abstract void fillAccountInfo(
+      Iterable<? extends AccountInfo> in,
+      Set<FillOptions> options)
+      throws DirectoryException;
+
+  @SuppressWarnings("serial")
+  public static class DirectoryException extends Exception {
+    public DirectoryException(String message) {
+      super(message);
+    }
+
+    public DirectoryException(String message, Throwable why) {
+      super(message, why);
+    }
+
+    public DirectoryException(Throwable why) {
+      super(why);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
index a296716..dfc9546 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
@@ -14,40 +14,45 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class AccountInfo {
   public static class Loader {
+    private static final Set<FillOptions> DETAILED_OPTIONS =
+        Collections.unmodifiableSet(EnumSet.of(
+            FillOptions.NAME,
+            FillOptions.EMAIL,
+            FillOptions.USERNAME,
+            FillOptions.AVATARS));
+
     public interface Factory {
       Loader create(boolean detailed);
     }
 
-    private final Provider<ReviewDb> db;
-    private final AccountCache accountCache;
+    private final InternalAccountDirectory directory;
     private final boolean detailed;
     private final Map<Account.Id, AccountInfo> created;
     private final List<AccountInfo> provided;
 
     @Inject
-    Loader(Provider<ReviewDb> db,
-        AccountCache accountCache,
-        @Assisted boolean detailed) {
-      this.db = db;
-      this.accountCache = accountCache;
+    Loader(InternalAccountDirectory directory, @Assisted boolean detailed) {
+      this.directory = directory;
       this.detailed = detailed;
       created = Maps.newHashMap();
       provided = Lists.newArrayList();
@@ -60,31 +65,29 @@
       AccountInfo info = created.get(id);
       if (info == null) {
         info = new AccountInfo(id);
+        if (detailed) {
+          info._account_id = id.get();
+        }
         created.put(id, info);
       }
       return info;
     }
 
     public void put(AccountInfo info) {
+      if (detailed) {
+        info._account_id = info._id.get();
+      }
       provided.add(info);
     }
 
     public void fill() throws OrmException {
-      Multimap<Account.Id, AccountInfo> missing = ArrayListMultimap.create();
-      for (AccountInfo info : Iterables.concat(created.values(), provided)) {
-        AccountState state = accountCache.getIfPresent(info._id);
-        if (state != null) {
-          info.fill(state.getAccount(), detailed);
-        } else {
-          missing.put(info._id, info);
-        }
-      }
-      if (!missing.isEmpty()) {
-        for (Account account : db.get().accounts().get(missing.keySet())) {
-          for (AccountInfo info : missing.get(account.getId())) {
-            info.fill(account, detailed);
-          }
-        }
+      try {
+        directory.fillAccountInfo(
+            Iterables.concat(created.values(), provided),
+            detailed ? DETAILED_OPTIONS : EnumSet.of(FillOptions.NAME));
+      } catch (DirectoryException e) {
+        Throwables.propagateIfPossible(e.getCause(), OrmException.class);
+        throw new OrmException(e);
       }
     }
 
@@ -97,12 +100,6 @@
     }
   }
 
-  public static AccountInfo parse(Account a, boolean detailed) {
-    AccountInfo ai = new AccountInfo(a.getId());
-    ai.fill(a, detailed);
-    return ai;
-  }
-
   public transient Account.Id _id;
 
   public AccountInfo(Account.Id id) {
@@ -112,12 +109,22 @@
   public Integer _account_id;
   public String name;
   public String email;
+  public String username;
+  public List<AvatarInfo> avatars;
 
-  private void fill(Account account, boolean detailed) {
-    name = account.getFullName();
-    if (detailed) {
-      _account_id = account.getId().get();
-      email = account.getPreferredEmail();
-    }
+  public static class AvatarInfo {
+    /**
+     * Size in pixels the UI prefers an avatar image to be.
+     *
+     * The web UI prefers avatar images to be square, both
+     * the height and width of the image should be this size.
+     * The height is the more important dimension to match
+     * than the width.
+     */
+    public static final int DEFAULT_SIZE = 26;
+
+    public String url;
+    public Integer height;
+    public Integer width;
   }
 }
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 1827446..f068812 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
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -232,12 +233,10 @@
         final AccountExternalId newId = createId(accountId, who);
         newId.setEmailAddress(who.getEmailAddress());
 
+        db.accountExternalIds().upsert(Collections.singleton(newId));
         if (openId.size() == 1) {
           final AccountExternalId oldId = openId.get(0);
-          db.accountExternalIds().upsert(Collections.singleton(newId));
           db.accountExternalIds().delete(Collections.singleton(oldId));
-        } else {
-          db.accountExternalIds().insert(Collections.singleton(newId));
         }
         return new AuthResult(accountId, newId.getKey(), false);
 
@@ -260,7 +259,7 @@
     }
 
     final Account.Id newId = new Account.Id(db.nextAccountId());
-    final Account account = new Account(newId);
+    final Account account = new Account(newId, TimeUtil.nowTs());
     final AccountExternalId extId = createId(newId, who);
 
     extId.setEmailAddress(who.getEmailAddress());
@@ -271,8 +270,8 @@
       && db.accounts().anyAccounts().toList().isEmpty();
 
     try {
-      db.accounts().insert(Collections.singleton(account));
-      db.accountExternalIds().insert(Collections.singleton(extId));
+      db.accounts().upsert(Collections.singleton(account));
+      db.accountExternalIds().upsert(Collections.singleton(extId));
     } finally {
       // If adding the account failed, it may be that it actually was the
       // first account. So we reset the 'check for first account'-guard, as
@@ -295,8 +294,8 @@
       final AccountGroup.Id adminId = g.getId();
       final AccountGroupMember m =
           new AccountGroupMember(new AccountGroupMember.Key(newId, adminId));
-      db.accountGroupMembersAudit().insert(
-          Collections.singleton(new AccountGroupMemberAudit(m, newId)));
+      db.accountGroupMembersAudit().insert(Collections.singleton(
+          new AccountGroupMemberAudit(m, newId, TimeUtil.nowTs())));
       db.accountGroupMembers().insert(Collections.singleton(m));
     }
 
@@ -386,46 +385,42 @@
    *         cannot be linked at this time.
    */
   public AuthResult link(final Account.Id to, AuthRequest who)
-      throws AccountException {
+      throws AccountException, OrmException {
+    final ReviewDb db = schema.open();
     try {
-      final ReviewDb db = schema.open();
-      try {
-        who = realm.link(db, to, who);
+      who = realm.link(db, to, who);
 
-        final AccountExternalId.Key key = id(who);
-        AccountExternalId extId = db.accountExternalIds().get(key);
-        if (extId != null) {
-          if (!extId.getAccountId().equals(to)) {
-            throw new AccountException("Identity in use by another account");
-          }
-          update(db, who, extId);
+      final AccountExternalId.Key key = id(who);
+      AccountExternalId extId = db.accountExternalIds().get(key);
+      if (extId != null) {
+        if (!extId.getAccountId().equals(to)) {
+          throw new AccountException("Identity in use by another account");
+        }
+        update(db, who, extId);
 
-        } else {
-          extId = createId(to, who);
-          extId.setEmailAddress(who.getEmailAddress());
-          db.accountExternalIds().insert(Collections.singleton(extId));
+      } else {
+        extId = createId(to, who);
+        extId.setEmailAddress(who.getEmailAddress());
+        db.accountExternalIds().insert(Collections.singleton(extId));
 
-          if (who.getEmailAddress() != null) {
-            final Account a = db.accounts().get(to);
-            if (a.getPreferredEmail() == null) {
-              a.setPreferredEmail(who.getEmailAddress());
-              db.accounts().update(Collections.singleton(a));
-            }
-          }
-
-          if (who.getEmailAddress() != null) {
-            byEmailCache.evict(who.getEmailAddress());
-            byIdCache.evict(to);
+        if (who.getEmailAddress() != null) {
+          final Account a = db.accounts().get(to);
+          if (a.getPreferredEmail() == null) {
+            a.setPreferredEmail(who.getEmailAddress());
+            db.accounts().update(Collections.singleton(a));
           }
         }
 
-        return new AuthResult(to, key, false);
-
-      } finally {
-        db.close();
+        if (who.getEmailAddress() != null) {
+          byEmailCache.evict(who.getEmailAddress());
+          byIdCache.evict(to);
+        }
       }
-    } catch (OrmException e) {
-      throw new AccountException("Cannot link identity", e);
+
+      return new AuthResult(to, key, false);
+
+    } finally {
+      db.close();
     }
   }
 
@@ -439,42 +434,38 @@
    *         cannot be unlinked at this time.
    */
   public AuthResult unlink(final Account.Id from, AuthRequest who)
-      throws AccountException {
+      throws AccountException, OrmException {
+    final ReviewDb db = schema.open();
     try {
-      final ReviewDb db = schema.open();
-      try {
-        who = realm.unlink(db, from, who);
+      who = realm.unlink(db, from, who);
 
-        final AccountExternalId.Key key = id(who);
-        AccountExternalId extId = db.accountExternalIds().get(key);
-        if (extId != null) {
-          if (!extId.getAccountId().equals(from)) {
-            throw new AccountException("Identity in use by another account");
+      final AccountExternalId.Key key = id(who);
+      AccountExternalId extId = db.accountExternalIds().get(key);
+      if (extId != null) {
+        if (!extId.getAccountId().equals(from)) {
+          throw new AccountException("Identity in use by another account");
+        }
+        db.accountExternalIds().delete(Collections.singleton(extId));
+
+        if (who.getEmailAddress() != null) {
+          final Account a = db.accounts().get(from);
+          if (a.getPreferredEmail() != null
+              && a.getPreferredEmail().equals(who.getEmailAddress())) {
+            a.setPreferredEmail(null);
+            db.accounts().update(Collections.singleton(a));
           }
-          db.accountExternalIds().delete(Collections.singleton(extId));
-
-          if (who.getEmailAddress() != null) {
-            final Account a = db.accounts().get(from);
-            if (a.getPreferredEmail() != null
-                && a.getPreferredEmail().equals(who.getEmailAddress())) {
-              a.setPreferredEmail(null);
-              db.accounts().update(Collections.singleton(a));
-            }
-            byEmailCache.evict(who.getEmailAddress());
-            byIdCache.evict(from);
-          }
-
-        } else {
-          throw new AccountException("Identity not found");
+          byEmailCache.evict(who.getEmailAddress());
+          byIdCache.evict(from);
         }
 
-        return new AuthResult(from, key, false);
-
-      } finally {
-        db.close();
+      } else {
+        throw new AccountException("Identity not found");
       }
-    } catch (OrmException e) {
-      throw new AccountException("Cannot unlink identity", e);
+
+      return new AuthResult(from, key, false);
+
+    } finally {
+      db.close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 67d87b3..383ed05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -56,7 +56,22 @@
    */
   public Account find(final String nameOrEmail) throws OrmException {
     Set<Account.Id> r = findAll(nameOrEmail);
-    return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
+    if (r.size() == 1) {
+      return byId.get(r.iterator().next()).getAccount();
+    }
+
+    Account match = null;
+    for (Account.Id id : r) {
+      Account account = byId.get(id).getAccount();
+      if (!account.isActive()) {
+        continue;
+      }
+      if (match != null) {
+        return null;
+      }
+      match = account;
+    }
+    return match;
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
index 9dc423a..106c033 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
@@ -16,7 +16,10 @@
 
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
 import com.google.inject.TypeLiteral;
 
 public class AccountResource implements RestResource {
@@ -26,6 +29,15 @@
   public static final TypeLiteral<RestView<Capability>> CAPABILITY_KIND =
       new TypeLiteral<RestView<Capability>>() {};
 
+  public static final TypeLiteral<RestView<Email>> EMAIL_KIND =
+      new TypeLiteral<RestView<Email>>() {};
+
+  public static final TypeLiteral<RestView<SshKey>> SSH_KEY_KIND =
+      new TypeLiteral<RestView<SshKey>>() {};
+
+  public static final TypeLiteral<RestView<StarredChange>> STARRED_CHANGE_KIND =
+      new TypeLiteral<RestView<StarredChange>>() {};
+
   private final IdentifiedUser user;
 
   public AccountResource(IdentifiedUser user) {
@@ -57,4 +69,43 @@
       return user.getCapabilities().canPerform(getCapability());
     }
   }
+
+  public static class Email extends AccountResource {
+    private final String email;
+
+    public Email(IdentifiedUser user, String email) {
+      super(user);
+      this.email = email;
+    }
+
+    public String getEmail() {
+      return email;
+    }
+  }
+
+  public static class SshKey extends AccountResource {
+    private final AccountSshKey sshKey;
+
+    public SshKey(IdentifiedUser user, AccountSshKey sshKey) {
+      super(user);
+      this.sshKey = sshKey;
+    }
+
+    public AccountSshKey getSshKey() {
+      return sshKey;
+    }
+  }
+
+  public static class StarredChange extends AccountResource {
+    private final ChangeResource change;
+
+    public StarredChange(IdentifiedUser user, ChangeResource change) {
+      super(user);
+      this.change = change;
+    }
+
+    public Change getChange() {
+      return change.getChange();
+    }
+  }
 }
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 674046c..7d3c06e 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
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -31,27 +31,29 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.util.Set;
-
 public class AccountsCollection implements
-    RestCollection<TopLevelResource, AccountResource> {
+    RestCollection<TopLevelResource, AccountResource>,
+    AcceptsCreate<TopLevelResource>{
   private final Provider<CurrentUser> self;
   private final AccountResolver resolver;
   private final AccountControl.Factory accountControlFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final DynamicMap<RestView<AccountResource>> views;
+  private final CreateAccount.Factory createAccountFactory;
 
   @Inject
   AccountsCollection(Provider<CurrentUser> self,
       AccountResolver resolver,
       AccountControl.Factory accountControlFactory,
       IdentifiedUser.GenericFactory userFactory,
-      DynamicMap<RestView<AccountResource>> views) {
+      DynamicMap<RestView<AccountResource>> views,
+      CreateAccount.Factory createAccountFactory) {
     this.self = self;
     this.resolver = resolver;
     this.accountControlFactory = accountControlFactory;
     this.userFactory = userFactory;
     this.views = views;
+    this.createAccountFactory = createAccountFactory;
   }
 
   @Override
@@ -73,7 +75,7 @@
    *        "Full Name <email@example.com>", just the email address, a full name
    *        if it is unique, an account ID, a user name or 'self' for the
    *        calling user
-   * @return the project
+   * @return the user, never null.
    * @throws UnprocessableEntityException thrown if the account ID cannot be
    *         resolved or if the account is not visible to the calling user
    */
@@ -91,7 +93,7 @@
     CurrentUser user = self.get();
 
     if (id.equals("self")) {
-      if (user instanceof IdentifiedUser) {
+      if (user.isIdentifiedUser()) {
         return (IdentifiedUser) user;
       } else if (user instanceof AnonymousUser) {
         throw new AuthException("Authentication required");
@@ -100,11 +102,11 @@
       }
     }
 
-    Set<Account.Id> matches = resolver.findAll(id);
-    if (matches.size() != 1) {
+    Account match = resolver.find(id);
+    if (match == null) {
       return null;
     }
-    return userFactory.create(Iterables.getOnlyElement(matches));
+    return userFactory.create(match.getId());
   }
 
   @Override
@@ -116,4 +118,10 @@
   public DynamicMap<RestView<AccountResource>> views() {
     return views;
   }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateAccount create(TopLevelResource parent, IdString username) {
+    return createAccountFactory.create(username.get());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
new file mode 100644
index 0000000..2cff009
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -0,0 +1,98 @@
+// 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.account;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteSource;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+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.RawInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AddSshKey.Input;
+import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+
+public class AddSshKey implements RestModifyView<AccountResource, Input> {
+  public static class Input {
+    public RawInput raw;
+  }
+
+  private final Provider<CurrentUser> self;
+  private final Provider<ReviewDb> dbProvider;
+  private final SshKeyCache sshKeyCache;
+
+  @Inject
+  AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
+      SshKeyCache sshKeyCache) {
+    this.self = self;
+    this.dbProvider = dbProvider;
+    this.sshKeyCache = sshKeyCache;
+  }
+
+  @Override
+  public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
+      throws AuthException, MethodNotAllowedException, BadRequestException,
+      ResourceConflictException, OrmException, IOException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to add SSH keys");
+    }
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.raw == null) {
+      throw new BadRequestException("SSH public key missing");
+    }
+
+    int max = 0;
+    for (AccountSshKey k : dbProvider.get().accountSshKeys()
+        .byAccount(rsrc.getUser().getAccountId())) {
+      max = Math.max(max, k.getKey().get());
+    }
+
+    final RawInput rawKey = input.raw;
+    String sshPublicKey = new ByteSource() {
+      @Override
+      public InputStream openStream() throws IOException {
+        return rawKey.getInputStream();
+      }
+    }.asCharSource(Charsets.UTF_8).read();
+
+    try {
+      AccountSshKey sshKey =
+          sshKeyCache.create(new AccountSshKey.Id(
+              rsrc.getUser().getAccountId(), max + 1), sshPublicKey);
+      dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
+      sshKeyCache.evict(rsrc.getUser().getUserName());
+      return Response.<SshKeyInfo>created(new SshKeyInfo(sshKey));
+    } catch (InvalidSshKeyException e) {
+      throw new BadRequestException(e.getMessage());
+    }
+  }
+}
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 d2014ec..cafc540 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
@@ -65,8 +65,12 @@
   /** @return true if the user can administer this server. */
   public boolean canAdministrateServer() {
     if (canAdministrateServer == null) {
-      canAdministrateServer = user instanceof PeerDaemonUser
-          || matchAny(capabilities.administrateServer, ALLOWED_RULE);
+      if (user.getRealUser() != user) {
+        canAdministrateServer = false;
+      } else {
+        canAdministrateServer = user instanceof PeerDaemonUser
+            || matchAny(capabilities.administrateServer, ALLOWED_RULE);
+      }
     }
     return canAdministrateServer;
   }
@@ -130,18 +134,11 @@
       || canAdministrateServer();
   }
 
-
   /** @return true if the user can access the database (with gsql). */
   public boolean canAccessDatabase() {
     return canPerform(GlobalCapability.ACCESS_DATABASE);
   }
 
-  /** @return true if the user can force replication to any configured destination. */
-  public boolean canStartReplication() {
-    return canPerform(GlobalCapability.START_REPLICATION)
-        || canAdministrateServer();
-  }
-
   /** @return true if the user can stream Gerrit events. */
   public boolean canStreamEvents() {
     return canPerform(GlobalCapability.STREAM_EVENTS)
@@ -154,6 +151,17 @@
         || canAdministrateServer();
   }
 
+  /** @return true if the user can generate HTTP passwords for users other than self. */
+  public boolean canGenerateHttpPassword() {
+    return canPerform(GlobalCapability.GENERATE_HTTP_PASSWORD)
+        || canAdministrateServer();
+  }
+
+  /** @return true if the user can impersonate another user. */
+  public boolean canRunAs() {
+    return canPerform(GlobalCapability.RUN_AS);
+  }
+
   /** @return which priority queue the user's tasks should be submitted to. */
   public QueueProvider.QueueType getQueueType() {
     // If a non-generic group (that is not Anonymous Users or Registered Users)
@@ -216,9 +224,18 @@
       List<PermissionRule> ruleList) {
     int min = 0;
     int max = 0;
-    for (PermissionRule rule : ruleList) {
-      min = Math.min(min, rule.getMin());
-      max = Math.max(max, rule.getMax());
+    if (ruleList.isEmpty()) {
+      PermissionRange.WithDefaults defaultRange =
+          GlobalCapability.getRange(permissionName);
+      if (defaultRange != null) {
+        min = defaultRange.getDefaultMin();
+        max = defaultRange.getDefaultMax();
+      }
+    } else {
+      for (PermissionRule rule : ruleList) {
+        min = Math.min(min, rule.getMin());
+        max = Math.max(max, rule.getMax());
+      }
     }
     return new PermissionRange(permissionName, min, max);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
new file mode 100644
index 0000000..6b68032
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
@@ -0,0 +1,85 @@
+// 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.account;
+
+import com.google.gerrit.extensions.annotations.CapabilityScope;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.annotation.Annotation;
+
+public class CapabilityUtils {
+  private static final Logger log = LoggerFactory
+      .getLogger(CapabilityUtils.class);
+
+  public static void checkRequiresCapability(Provider<CurrentUser> userProvider,
+      String pluginName, Class<?> clazz)
+      throws AuthException {
+    RequiresCapability rc = getClassAnnotation(clazz, RequiresCapability.class);
+    if (rc != null) {
+      CurrentUser user = userProvider.get();
+      CapabilityControl ctl = user.getCapabilities();
+      if (ctl.canAdministrateServer()) {
+        return;
+      }
+
+      String capability = rc.value();
+      if (pluginName != null && !"gerrit".equals(pluginName)
+         && (rc.scope() == CapabilityScope.PLUGIN
+          || rc.scope() == CapabilityScope.CONTEXT)) {
+        capability = String.format("%s-%s", pluginName, rc.value());
+      } else if (rc.scope() == CapabilityScope.PLUGIN) {
+        log.error(String.format(
+            "Class %s uses @%s(scope=%s), but is not within a plugin",
+            clazz.getName(),
+            RequiresCapability.class.getSimpleName(),
+            CapabilityScope.PLUGIN.name()));
+        throw new AuthException("cannot check capability");
+      }
+
+      if (!ctl.canPerform(capability)) {
+        throw new AuthException(String.format(
+            "Capability %s is required to access this resource",
+            capability));
+      }
+    }
+  }
+
+  /**
+   * Find an instance of the specified annotation, walking up the inheritance
+   * tree if necessary.
+   *
+   * @param <T> Annotation type to search for
+   * @param clazz root class to search, may be null
+   * @param annotationClass class object of Annotation subclass to search for
+   * @return the requested annotation or null if none
+   */
+  private static <T extends Annotation> T getClassAnnotation(Class<?> clazz,
+      Class<T> annotationClass) {
+    for (; clazz != null; clazz = clazz.getSuperclass()) {
+      T t = clazz.getAnnotation(annotationClass);
+      if (t != null) {
+        return t;
+      }
+    }
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index 1b73c54..1210906 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -36,8 +37,6 @@
 import java.util.concurrent.Callable;
 import java.util.regex.Pattern;
 
-import javax.annotation.Nullable;
-
 /** Operation to change the username of an account. */
 public class ChangeUserName implements Callable<VoidResult> {
   private static final Pattern USER_NAME_PATTERN =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
deleted file mode 100644
index 255c248..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collections;
-import java.util.concurrent.Callable;
-
-/** Operation to clear a password for an account. */
-public class ClearPassword implements Callable<AccountExternalId> {
-  public interface Factory {
-    ClearPassword create(AccountExternalId.Key forUser);
-  }
-
-  private final AccountCache accountCache;
-  private final ReviewDb db;
-  private final IdentifiedUser user;
-
-  private final AccountExternalId.Key forUser;
-
-  @Inject
-  ClearPassword(final AccountCache accountCache, final ReviewDb db,
-      final IdentifiedUser user,
-
-      @Assisted AccountExternalId.Key forUser) {
-    this.accountCache = accountCache;
-    this.db = db;
-    this.user = user;
-
-    this.forUser = forUser;
-  }
-
-  public AccountExternalId call() throws OrmException, NoSuchEntityException {
-    AccountExternalId id = db.accountExternalIds().get(forUser);
-    if (id == null || !user.getAccountId().equals(id.getAccountId())) {
-      throw new NoSuchEntityException();
-    }
-
-    id.setPassword(null);
-    db.accountExternalIds().update(Collections.singleton(id));
-    accountCache.evict(user.getAccountId());
-    return id;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
new file mode 100644
index 0000000..340746e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -0,0 +1,208 @@
+// 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.account;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.CreateAccount.Input;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
+  public static class Input {
+    @DefaultInput
+    public String username;
+    public String name;
+    public String email;
+    public String sshKey;
+    public String httpPassword;
+    public List<String> groups;
+  }
+
+  public static interface Factory {
+    CreateAccount create(String username);
+  }
+
+  private final ReviewDb db;
+  private final IdentifiedUser currentUser;
+  private final GroupsCollection groupsCollection;
+  private final SshKeyCache sshKeyCache;
+  private final AccountCache accountCache;
+  private final AccountByEmailCache byEmailCache;
+  private final AccountInfo.Loader.Factory infoLoader;
+  private final String username;
+
+  @Inject
+  CreateAccount(ReviewDb db, IdentifiedUser currentUser,
+      GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
+      AccountCache accountCache, AccountByEmailCache byEmailCache,
+      AccountInfo.Loader.Factory infoLoader,
+      @Assisted String username) {
+    this.db = db;
+    this.currentUser = currentUser;
+    this.groupsCollection = groupsCollection;
+    this.sshKeyCache = sshKeyCache;
+    this.accountCache = accountCache;
+    this.byEmailCache = byEmailCache;
+    this.infoLoader = infoLoader;
+    this.username = username;
+  }
+
+  @Override
+  public Object apply(TopLevelResource rsrc, Input input)
+      throws BadRequestException, ResourceConflictException,
+      UnprocessableEntityException, OrmException {
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.username != null && !username.equals(input.username)) {
+      throw new BadRequestException("username must match URL");
+    }
+
+    if (!username.matches(Account.USER_NAME_PATTERN)) {
+      throw new BadRequestException("Username '" + username + "'"
+          + " must contain only letters, numbers, _, - or .");
+    }
+
+    Set<AccountGroup.Id> groups = parseGroups(input.groups);
+
+    Account.Id id = new Account.Id(db.nextAccountId());
+    AccountSshKey key = createSshKey(id, input.sshKey);
+
+    AccountExternalId extUser =
+        new AccountExternalId(id, new AccountExternalId.Key(
+            AccountExternalId.SCHEME_USERNAME, username));
+
+    if (input.httpPassword != null) {
+      extUser.setPassword(input.httpPassword);
+    }
+
+    if (db.accountExternalIds().get(extUser.getKey()) != null) {
+      throw new ResourceConflictException(
+          "username '" + username + "' already exists");
+    }
+    if (input.email != null
+        && db.accountExternalIds().get(getEmailKey(input.email)) != null) {
+      throw new UnprocessableEntityException(
+          "email '" + input.email + "' already exists");
+    }
+
+    try {
+      db.accountExternalIds().insert(Collections.singleton(extUser));
+    } catch (OrmDuplicateKeyException duplicateKey) {
+      throw new ResourceConflictException(
+          "username '" + username + "' already exists");
+    }
+
+    if (input.email != null) {
+      AccountExternalId extMailto =
+          new AccountExternalId(id, getEmailKey(input.email));
+      extMailto.setEmailAddress(input.email);
+      try {
+        db.accountExternalIds().insert(Collections.singleton(extMailto));
+      } catch (OrmDuplicateKeyException duplicateKey) {
+        try {
+          db.accountExternalIds().delete(Collections.singleton(extUser));
+        } catch (OrmException cleanupError) {
+        }
+        throw new UnprocessableEntityException(
+            "email '" + input.email + "' already exists");
+      }
+    }
+
+    Account a = new Account(id, TimeUtil.nowTs());
+    a.setFullName(input.name);
+    a.setPreferredEmail(input.email);
+    db.accounts().insert(Collections.singleton(a));
+
+    if (key != null) {
+      db.accountSshKeys().insert(Collections.singleton(key));
+    }
+
+    for (AccountGroup.Id groupId : groups) {
+      AccountGroupMember m =
+          new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
+      db.accountGroupMembersAudit().insert(Collections.singleton(
+          new AccountGroupMemberAudit(
+              m, currentUser.getAccountId(), TimeUtil.nowTs())));
+      db.accountGroupMembers().insert(Collections.singleton(m));
+    }
+
+    sshKeyCache.evict(username);
+    accountCache.evictByUsername(username);
+    byEmailCache.evict(input.email);
+
+    AccountInfo.Loader loader = infoLoader.create(true);
+    AccountInfo info = loader.get(id);
+    loader.fill();
+    return Response.created(info);
+  }
+
+  private Set<AccountGroup.Id> parseGroups(List<String> groups)
+      throws UnprocessableEntityException {
+    Set<AccountGroup.Id> groupIds = Sets.newHashSet();
+    if (groups != null) {
+      for (String g : groups) {
+        groupIds.add(GroupDescriptions.toAccountGroup(
+            groupsCollection.parseInternal(g)).getId());
+      }
+    }
+    return groupIds;
+  }
+
+  private AccountSshKey createSshKey(Account.Id id, String sshKey)
+      throws BadRequestException {
+    if (sshKey == null) {
+      return null;
+    }
+    try {
+      return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
+    } catch (InvalidSshKeyException e) {
+      throw new BadRequestException(e.getMessage());
+    }
+  }
+
+  private AccountExternalId.Key getEmailKey(String email) {
+    return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
new file mode 100644
index 0000000..4fda74c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -0,0 +1,137 @@
+// 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.account;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CreateEmail.Input;
+import com.google.gerrit.server.account.GetEmails.EmailInfo;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CreateEmail implements RestModifyView<AccountResource, Input> {
+  private final Logger log = LoggerFactory.getLogger(getClass());
+
+  public static class Input {
+    @DefaultInput
+    public String email;
+    public boolean preferred;
+    public boolean noConfirmation;
+  }
+
+  public static interface Factory {
+    CreateEmail create(String email);
+  }
+
+  private final Provider<CurrentUser> self;
+  private final Realm realm;
+  private final AuthConfig authConfig;
+  private final AccountManager accountManager;
+  private final RegisterNewEmailSender.Factory registerNewEmailFactory;
+  private final Provider<PutPreferred> putPreferredProvider;
+  private final String email;
+
+  @Inject
+  CreateEmail(Provider<CurrentUser> self,
+      Realm realm,
+      AuthConfig authConfig,
+      AccountManager accountManager,
+      RegisterNewEmailSender.Factory registerNewEmailFactory,
+      Provider<PutPreferred> putPreferredProvider,
+      @Assisted String email) {
+    this.self = self;
+    this.realm = realm;
+    this.authConfig = authConfig;
+    this.accountManager = accountManager;
+    this.registerNewEmailFactory = registerNewEmailFactory;
+    this.putPreferredProvider = putPreferredProvider;
+    this.email = email;
+  }
+
+  @Override
+  public Object apply(AccountResource rsrc, Input input) throws AuthException,
+      BadRequestException, ResourceConflictException,
+      ResourceNotFoundException, OrmException, EmailException,
+      MethodNotAllowedException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to add email address");
+    }
+
+    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+      throw new MethodNotAllowedException("realm does not allow adding emails");
+    }
+
+    if (input == null) {
+      input = new Input();
+    }
+
+    if (input.email != null && !email.equals(input.email)) {
+      throw new BadRequestException("email address must match URL");
+    }
+
+    if (input.noConfirmation
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("must be administrator to use no_confirmation");
+    }
+
+    EmailInfo info = new EmailInfo();
+    info.email = email;
+    if (input.noConfirmation
+        || authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+      try {
+        accountManager.link(rsrc.getUser().getAccountId(),
+            AuthRequest.forEmail(email));
+      } catch (AccountException e) {
+        throw new ResourceConflictException(e.getMessage());
+      }
+      if (input.preferred) {
+        putPreferredProvider.get().apply(
+            new AccountResource.Email(rsrc.getUser(), email),
+            null);
+        info.preferred = true;
+      }
+    } else {
+      try {
+        registerNewEmailFactory.create(email).send();
+        info.pendingConfirmation = true;
+      } catch (EmailException e) {
+        log.error("Cannot send email verification message to " + email, e);
+        throw e;
+      } catch (RuntimeException e) {
+        log.error("Cannot send email verification message to " + email, e);
+        throw e;
+      }
+    }
+    return Response.created(info);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index c90f3e9..d9fb303 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 
 import java.util.Set;
@@ -23,17 +26,32 @@
 public class DefaultRealm implements Realm {
   private final EmailExpander emailExpander;
   private final AccountByEmailCache byEmail;
+  private final AuthConfig authConfig;
 
   @Inject
   DefaultRealm(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail) {
+      final AccountByEmailCache byEmail, final AuthConfig authConfig) {
     this.emailExpander = emailExpander;
     this.byEmail = byEmail;
+    this.authConfig = authConfig;
   }
 
   @Override
   public boolean allowsEdit(final Account.FieldName field) {
-    return true;
+    if (authConfig.getAuthType() == AuthType.HTTP) {
+      switch (field) {
+        case USER_NAME:
+          return false;
+        case FULL_NAME:
+          return Strings.emptyToNull(authConfig.getHttpDisplaynameHeader()) == null;
+        case REGISTER_NEW_EMAIL:
+          return Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
+        default:
+          return true;
+      }
+    } else {
+      return true;
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
new file mode 100644
index 0000000..d44bc2c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -0,0 +1,60 @@
+// 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.account;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+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.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.DeleteActive.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class DeleteActive implements RestModifyView<AccountResource, Input> {
+  public static class Input {
+  }
+
+  private final Provider<ReviewDb> dbProvider;
+  private final AccountCache byIdCache;
+
+  @Inject
+  DeleteActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+    this.dbProvider = dbProvider;
+    this.byIdCache = byIdCache;
+  }
+
+  @Override
+  public Object apply(AccountResource rsrc, Input input)
+      throws ResourceNotFoundException, OrmException {
+    Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+    if (a == null) {
+      throw new ResourceNotFoundException("account not found");
+    }
+    if (!a.isActive()) {
+      throw new ResourceNotFoundException();
+    }
+    a.setActive(false);
+    dbProvider.get().accounts().update(Collections.singleton(a));
+    byIdCache.evict(a.getId());
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
new file mode 100644
index 0000000..4b38b9f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -0,0 +1,75 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.DeleteEmail.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
+  public static class Input {
+  }
+
+  private final Provider<CurrentUser> self;
+  private final Realm realm;
+  private final Provider<ReviewDb> dbProvider;
+  private final AccountManager accountManager;
+
+  @Inject
+  DeleteEmail(Provider<CurrentUser> self, Realm realm,
+      Provider<ReviewDb> dbProvider, AccountManager accountManager) {
+    this.self = self;
+    this.realm = realm;
+    this.dbProvider = dbProvider;
+    this.accountManager = accountManager;
+  }
+
+  @Override
+  public Object apply(AccountResource.Email rsrc, Input input)
+      throws AuthException, ResourceNotFoundException,
+      ResourceConflictException, MethodNotAllowedException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to delete email address");
+    }
+    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+      throw new MethodNotAllowedException("realm does not allow deleting emails");
+    }
+    AccountExternalId.Key key = new AccountExternalId.Key(
+        AccountExternalId.SCHEME_MAILTO, rsrc.getEmail());
+    AccountExternalId extId = dbProvider.get().accountExternalIds().get(key);
+    if (extId == null) {
+      throw new ResourceNotFoundException(rsrc.getEmail());
+    }
+    try {
+      accountManager.unlink(rsrc.getUser().getAccountId(),
+          AuthRequest.forEmail(rsrc.getEmail()));
+    } catch (AccountException e) {
+      throw new ResourceConflictException(e.getMessage());
+    }
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
new file mode 100644
index 0000000..cf60df1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
@@ -0,0 +1,50 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.DeleteSshKey.Input;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class DeleteSshKey implements
+    RestModifyView<AccountResource.SshKey, Input> {
+  public static class Input {
+  }
+
+  private final Provider<ReviewDb> dbProvider;
+  private final SshKeyCache sshKeyCache;
+
+  @Inject
+  DeleteSshKey(Provider<ReviewDb> dbProvider, SshKeyCache sshKeyCache) {
+    this.dbProvider = dbProvider;
+    this.sshKeyCache = sshKeyCache;
+  }
+
+  @Override
+  public Object apply(AccountResource.SshKey rsrc, Input input)
+      throws OrmException {
+    dbProvider.get().accountSshKeys()
+        .deleteKeys(Collections.singleton(rsrc.getSshKey().getKey()));
+    sshKeyCache.evict(rsrc.getUser().getUserName());
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
new file mode 100644
index 0000000..f523e15
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
@@ -0,0 +1,84 @@
+// 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.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResource.Email;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Emails implements
+    ChildCollection<AccountResource, AccountResource.Email>,
+    AcceptsCreate<AccountResource> {
+  private final DynamicMap<RestView<AccountResource.Email>> views;
+  private final Provider<GetEmails> list;
+  private final Provider<CurrentUser> self;
+  private final CreateEmail.Factory createEmailFactory;
+
+  @Inject
+  Emails(DynamicMap<RestView<AccountResource.Email>> views,
+      Provider<GetEmails> list,
+      Provider<CurrentUser> self,
+      CreateEmail.Factory createEmailFactory) {
+    this.views = views;
+    this.list = list;
+    this.self = self;
+    this.createEmailFactory = createEmailFactory;
+  }
+
+  @Override
+  public RestView<AccountResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public AccountResource.Email parse(AccountResource rsrc, IdString id)
+      throws ResourceNotFoundException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new ResourceNotFoundException();
+    }
+
+    if ("preferred".equals(id.get())) {
+      String email = rsrc.getUser().getAccount().getPreferredEmail();
+      if (Strings.isNullOrEmpty(email)) {
+        throw new ResourceNotFoundException();
+      }
+      return new AccountResource.Email(rsrc.getUser(), email);
+    } else if (rsrc.getUser().getEmailAddresses().contains(id.get())) {
+      return new AccountResource.Email(rsrc.getUser(), id.get());
+    } else {
+      throw new ResourceNotFoundException();
+    }
+  }
+
+  @Override
+  public DynamicMap<RestView<Email>> views() {
+    return views;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateEmail create(AccountResource parent, IdString email) {
+    return createEmailFactory.create(email.get());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
deleted file mode 100644
index bbab126..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.apache.commons.codec.binary.Base64;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Collections;
-import java.util.concurrent.Callable;
-
-/** Operation to generate a password for an account. */
-public class GeneratePassword implements Callable<AccountExternalId> {
-  private static final int LEN = 12;
-  private static final SecureRandom rng;
-
-  static {
-    try {
-      rng = SecureRandom.getInstance("SHA1PRNG");
-    } catch (NoSuchAlgorithmException e) {
-      throw new RuntimeException("Cannot create RNG for password generator", e);
-    }
-  }
-
-  public interface Factory {
-    GeneratePassword create(AccountExternalId.Key forUser);
-  }
-
-  private final AccountCache accountCache;
-  private final ReviewDb db;
-  private final IdentifiedUser user;
-
-  private final AccountExternalId.Key forUser;
-
-  @Inject
-  GeneratePassword(final AccountCache accountCache, final ReviewDb db,
-      final IdentifiedUser user,
-
-      @Assisted AccountExternalId.Key forUser) {
-    this.accountCache = accountCache;
-    this.db = db;
-    this.user = user;
-
-    this.forUser = forUser;
-  }
-
-  public AccountExternalId call() throws OrmException, NoSuchEntityException {
-    AccountExternalId id = db.accountExternalIds().get(forUser);
-    if (id == null || !user.getAccountId().equals(id.getAccountId())) {
-      throw new NoSuchEntityException();
-    }
-
-    id.setPassword(generate());
-    db.accountExternalIds().update(Collections.singleton(id));
-    accountCache.evict(user.getAccountId());
-    return id;
-  }
-
-  private String generate() {
-    byte[] rand = new byte[LEN];
-    rng.nextBytes(rand);
-
-    byte[] enc = Base64.encodeBase64(rand, false);
-    StringBuilder r = new StringBuilder(LEN);
-    for (int i = 0; i < LEN; i++) {
-      if (enc[i] == '=') {
-        break;
-      }
-      r.append((char) enc[i]);
-    }
-    return r.toString();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
index b022420..f990b5b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
@@ -15,10 +15,22 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
 public class GetAccount implements RestReadView<AccountResource> {
+  private final AccountInfo.Loader.Factory infoFactory;
+
+  @Inject
+  GetAccount(AccountInfo.Loader.Factory infoFactory) {
+    this.infoFactory = infoFactory;
+  }
+
   @Override
-  public AccountInfo apply(AccountResource rsrc) {
-    return AccountInfo.parse(rsrc.getUser().getAccount(), true);
+  public AccountInfo apply(AccountResource rsrc) throws OrmException {
+    AccountInfo.Loader loader = infoFactory.create(true);
+    AccountInfo info = loader.get(rsrc.getUser().getAccountId());
+    loader.fill();
+    return info;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
new file mode 100644
index 0000000..76c7ddb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
@@ -0,0 +1,29 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetActive implements RestReadView<AccountResource> {
+  @Override
+  public Object apply(AccountResource rsrc) throws ResourceNotFoundException {
+    if (rsrc.getUser().getAccount().isActive()) {
+      return Response.ok("");
+    }
+    throw new ResourceNotFoundException();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
index 1c66555..a96e713 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -24,6 +25,8 @@
 
 import org.kohsuke.args4j.Option;
 
+import java.util.concurrent.TimeUnit;
+
 class GetAvatar implements RestReadView<AccountResource> {
   private final DynamicItem<AvatarProvider> avatarProvider;
 
@@ -41,12 +44,14 @@
       throws ResourceNotFoundException {
     AvatarProvider impl = avatarProvider.get();
     if (impl == null) {
-      throw new ResourceNotFoundException();
+      throw (new ResourceNotFoundException())
+          .caching(CacheControl.PUBLIC(1, TimeUnit.DAYS));
     }
 
     String url = impl.getUrl(rsrc.getUser(), size);
     if (Strings.isNullOrEmpty(url)) {
-      throw new ResourceNotFoundException();
+      throw (new ResourceNotFoundException())
+          .caching(CacheControl.PUBLIC(1, TimeUnit.HOURS));
     } else {
       return Response.redirect(url);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 54f1980..615d09e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -23,7 +23,6 @@
 import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
 import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
-import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
 import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
@@ -34,6 +33,8 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+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.BinaryResult;
@@ -68,10 +69,13 @@
   private Set<String> query;
 
   private final Provider<CurrentUser> self;
+  private final DynamicMap<CapabilityDefinition> pluginCapabilities;
 
   @Inject
-  GetCapabilities(Provider<CurrentUser> self) {
+  GetCapabilities(Provider<CurrentUser> self,
+      DynamicMap<CapabilityDefinition> pluginCapabilities) {
     this.self = self;
+    this.pluginCapabilities = pluginCapabilities;
   }
 
   @Override
@@ -93,6 +97,14 @@
         }
       }
     }
+    for (String pluginName : pluginCapabilities.plugins()) {
+      for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
+        String name = String.format("%s-%s", pluginName, capability);
+        if (want(name) && cc.canPerform(name)) {
+          have.put(name, true);
+        }
+      }
+    }
 
     have.put(CREATE_ACCOUNT, cc.canCreateAccount());
     have.put(CREATE_GROUP, cc.canCreateGroup());
@@ -104,7 +116,6 @@
     have.put(VIEW_CONNECTIONS, cc.canViewConnections());
     have.put(VIEW_QUEUE, cc.canViewQueue());
     have.put(RUN_GC, cc.canRunGC());
-    have.put(START_REPLICATION, cc.canStartReplication());
     have.put(STREAM_EVENTS, cc.canStreamEvents());
     have.put(ACCESS_DATABASE, cc.canAccessDatabase());
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java
new file mode 100644
index 0000000..c56a0a0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java
@@ -0,0 +1,28 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.GetEmails.EmailInfo;
+
+public class GetEmail implements RestReadView<AccountResource.Email> {
+  @Override
+  public EmailInfo apply(AccountResource.Email rsrc) {
+    EmailInfo e = new EmailInfo();
+    e.email = rsrc.getEmail();
+    e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
+    return e;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
new file mode 100644
index 0000000..0e0e77a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
@@ -0,0 +1,72 @@
+// 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.account;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class GetEmails implements RestReadView<AccountResource> {
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  public GetEmails(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public List<EmailInfo> apply(AccountResource rsrc) throws AuthException,
+      OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to list email addresses");
+    }
+
+    List<EmailInfo> emails = Lists.newArrayList();
+    for (String email : rsrc.getUser().getEmailAddresses()) {
+      if (email != null) {
+        EmailInfo e = new EmailInfo();
+        e.email = email;
+        e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
+        emails.add(e);
+      }
+    }
+    Collections.sort(emails, new Comparator<EmailInfo>() {
+      @Override
+      public int compare(EmailInfo a, EmailInfo b) {
+        return a.email.compareTo(b.email);
+      }
+    });
+    return emails;
+  }
+
+  public static class EmailInfo {
+    public String email;
+    public Boolean preferred;
+    public Boolean pendingConfirmation;
+
+    void preferred(String e) {
+      this.preferred = e != null && e.equals(email) ? true : null;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
new file mode 100644
index 0000000..8eaf4b3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
@@ -0,0 +1,51 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetHttpPassword implements RestReadView<AccountResource> {
+
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GetHttpPassword(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public String apply(AccountResource rsrc) throws AuthException,
+      ResourceNotFoundException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to get http password");
+    }
+    AccountState s = rsrc.getUser().state();
+    if (s.getUserName() == null) {
+      throw new ResourceNotFoundException();
+    }
+    String p = s.getPassword(s.getUserName());
+    if (p == null) {
+      throw new ResourceNotFoundException();
+    }
+    return p;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java
new file mode 100644
index 0000000..646a3b2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java
@@ -0,0 +1,25 @@
+// 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.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetName implements RestReadView<AccountResource> {
+  @Override
+  public String apply(AccountResource rsrc) {
+    return Strings.nullToEmpty(rsrc.getUser().getAccount().getFullName());
+  }
+}
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
new file mode 100644
index 0000000..4d3b913
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -0,0 +1,84 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ChangeScreen;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetPreferences implements RestReadView<AccountResource> {
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GetPreferences(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public PreferenceInfo apply(AccountResource rsrc) throws AuthException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+    return new PreferenceInfo(rsrc.getUser().getAccount()
+        .getGeneralPreferences());
+  }
+
+  static class PreferenceInfo {
+    final String kind = "gerritcodereview#preferences";
+
+    short changesPerPage;
+    Boolean showSiteHeader;
+    Boolean useFlashClipboard;
+    DownloadScheme downloadScheme;
+    DownloadCommand downloadCommand;
+    Boolean copySelfOnEmail;
+    DateFormat dateFormat;
+    TimeFormat timeFormat;
+    Boolean reversePatchSetOrder;
+    Boolean showUsernameInReviewCategory;
+    Boolean relativeDateInChangeTable;
+    CommentVisibilityStrategy commentVisibilityStrategy;
+    DiffView diffView;
+    ChangeScreen changeScreen;
+
+    PreferenceInfo(AccountGeneralPreferences p) {
+      changesPerPage = p.getMaximumPageSize();
+      showSiteHeader = p.isShowSiteHeader() ? true : null;
+      useFlashClipboard = p.isUseFlashClipboard() ? true : null;
+      downloadScheme = p.getDownloadUrl();
+      downloadCommand = p.getDownloadCommand();
+      copySelfOnEmail = p.isCopySelfOnEmails() ? true : null;
+      dateFormat = p.getDateFormat();
+      timeFormat = p.getTimeFormat();
+      reversePatchSetOrder = p.isReversePatchSetOrder() ? true : null;
+      showUsernameInReviewCategory = p.isShowUsernameInReviewCategory() ? true : null;
+      relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
+      commentVisibilityStrategy = p.getCommentVisibilityStrategy();
+      diffView = p.getDiffView();
+      changeScreen = p.getChangeScreen();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java
new file mode 100644
index 0000000..37445e9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java
@@ -0,0 +1,27 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountResource.SshKey;
+import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+
+public class GetSshKey implements RestReadView<AccountResource.SshKey> {
+
+  @Override
+  public SshKeyInfo apply(SshKey rsrc) {
+    return new SshKeyInfo(rsrc.getSshKey());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
new file mode 100644
index 0000000..f198b77
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -0,0 +1,74 @@
+// 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.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.List;
+
+public class GetSshKeys implements RestReadView<AccountResource> {
+
+  private final Provider<CurrentUser> self;
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  GetSshKeys(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+    this.self = self;
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException,
+      OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to get SSH keys");
+    }
+
+    List<SshKeyInfo> sshKeys = Lists.newArrayList();
+    for (AccountSshKey sshKey : dbProvider.get().accountSshKeys()
+        .byAccount(rsrc.getUser().getAccountId()).toList()) {
+      sshKeys.add(new SshKeyInfo(sshKey));
+    }
+    return sshKeys;
+  }
+
+  public static class SshKeyInfo {
+    public SshKeyInfo(AccountSshKey sshKey) {
+      seq = sshKey.getKey().get();
+      sshPublicKey = sshKey.getSshPublicKey();
+      encodedKey = sshKey.getEncodedKey();
+      algorithm = sshKey.getAlgorithm();
+      comment = Strings.emptyToNull(sshKey.getComment());
+      valid = sshKey.isValid();
+    }
+
+    public int seq;
+    public String sshPublicKey;
+    public String encodedKey;
+    public String algorithm;
+    public String comment;
+    public boolean valid;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java
new file mode 100644
index 0000000..8dcb236
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java
@@ -0,0 +1,46 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetUsername implements RestReadView<AccountResource> {
+
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GetUsername(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public String apply(AccountResource rsrc) throws AuthException,
+      ResourceNotFoundException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to get username");
+    }
+    String username = rsrc.getUser().getAccount().getUserName();
+    if (username == null) {
+      throw new ResourceNotFoundException();
+    }
+    return username;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
index 34db967..43b94f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
@@ -23,8 +24,6 @@
 
 import java.util.Collection;
 
-import javax.annotation.Nullable;
-
 /**
  * Implementations of GroupBackend provide lookup and membership accessors
  * to a group system.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
index f7e0634..69ca1e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
@@ -15,15 +15,13 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectControl;
 
 import java.util.Collection;
 import java.util.Comparator;
 
-import javax.annotation.Nullable;
-
 /**
  * Utility class for dealing with a GroupBackend.
  */
@@ -38,8 +36,8 @@
   };
 
   /**
-   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
-   * the best suggestion, or null if one does not exist.
+   * Runs {@link GroupBackend#suggest(String, ProjectControl)} and filters the
+   * result to return the best suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
    * @param name the name for which to suggest groups
@@ -50,9 +48,10 @@
       String name) {
     return findBestSuggestion(groupBackend, name, null);
   }
+
   /**
-   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
-   * the best suggestion, or null if one does not exist.
+   * Runs {@link GroupBackend#suggest(String, ProjectControl)} and filters the
+   * result to return the best suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
    * @param name the name for which to suggest groups
@@ -76,8 +75,8 @@
   }
 
   /**
-   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
-   * the exact suggestion, or null if one does not exist.
+   * Runs {@link GroupBackend#suggest(String, ProjectControl)} and filters the
+   * result to return the exact suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
    * @param name the name for which to suggest groups
@@ -90,8 +89,8 @@
   }
 
   /**
-   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
-   * the exact suggestion, or null if one does not exist.
+   * Runs {@link GroupBackend#suggest(String, ProjectControl)} and filters the
+   * result to return the exact suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
    * @param name the name for which to suggest groups
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index 3b9e85f..c1a4e0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
-import javax.annotation.Nullable;
-
 /** Tracks group objects in memory for efficient access. */
 public interface GroupCache {
   public AccountGroup get(AccountGroup.Id groupId);
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 2e01f26..ede2431 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
@@ -159,7 +159,7 @@
   }
 
   public boolean canSeeMember(Account.Id id) {
-    if (user instanceof IdentifiedUser
+    if (user.isIdentifiedUser()
         && ((IdentifiedUser) user).getAccountId().equals(id)) {
       return true;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index cade9c7..4d74e4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
@@ -122,19 +122,19 @@
     return members;
   }
 
-  private List<AccountGroupIncludeByUuid> loadIncludes() throws OrmException {
-    List<AccountGroupIncludeByUuid> groups = new ArrayList<AccountGroupIncludeByUuid>();
+  private List<AccountGroupById> loadIncludes() throws OrmException {
+    List<AccountGroupById> groups = new ArrayList<AccountGroupById>();
 
-    for (final AccountGroupIncludeByUuid m : db.accountGroupIncludesByUuid().byGroup(groupId)) {
+    for (final AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
       if (control.canSeeGroup(m.getIncludeUUID())) {
         gic.want(m.getIncludeUUID());
         groups.add(m);
       }
     }
 
-    Collections.sort(groups, new Comparator<AccountGroupIncludeByUuid>() {
-      public int compare(final AccountGroupIncludeByUuid o1,
-          final AccountGroupIncludeByUuid o2) {
+    Collections.sort(groups, new Comparator<AccountGroupById>() {
+      public int compare(final AccountGroupById o1,
+          final AccountGroupById o2) {
         GroupDescription.Basic a = gic.get(o1.getIncludeUUID());
         GroupDescription.Basic b = gic.get(o2.getIncludeUUID());
         return n(a).compareTo(n(b));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index bbcdfaf..37d407c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gwtorm.server.SchemaFactory;
@@ -152,7 +152,7 @@
         }
 
         Set<AccountGroup.UUID> ids = Sets.newHashSet();
-        for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
+        for (AccountGroupById agi : db.accountGroupById()
             .byGroup(group.get(0).getId())) {
           ids.add(agi.getIncludeUUID());
         }
@@ -177,7 +177,7 @@
       final ReviewDb db = schema.open();
       try {
         Set<AccountGroup.Id> ids = Sets.newHashSet();
-        for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
+        for (AccountGroupById agi : db.accountGroupById()
             .byIncludeUUID(key)) {
           ids.add(agi.getGroupId());
         }
@@ -207,7 +207,7 @@
       final ReviewDb db = schema.open();
       try {
         Set<AccountGroup.UUID> ids = Sets.newHashSet();
-        for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid().all()) {
+        for (AccountGroupById agi : db.accountGroupById().all()) {
           if (!AccountGroup.isInternalGroup(agi.getIncludeUUID())) {
             ids.add(agi.getIncludeUUID());
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index 08cf1a7..b6f7b48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
@@ -28,6 +28,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -58,13 +59,13 @@
 
   public Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
       final Project.NameKey project) throws NoSuchGroupException,
-      NoSuchProjectException, OrmException {
+      NoSuchProjectException, OrmException, IOException {
     return listAccounts(groupUUID, project, new HashSet<AccountGroup.UUID>());
   }
 
   private Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
       final Project.NameKey project, final Set<AccountGroup.UUID> seen)
-      throws NoSuchGroupException, OrmException, NoSuchProjectException {
+      throws NoSuchGroupException, OrmException, NoSuchProjectException, IOException {
     if (AccountGroup.PROJECT_OWNERS.equals(groupUUID)) {
       return getProjectOwners(project, seen);
     } else {
@@ -79,7 +80,7 @@
 
   private Set<Account> getProjectOwners(final Project.NameKey project,
       final Set<AccountGroup.UUID> seen) throws NoSuchProjectException,
-      NoSuchGroupException, OrmException {
+      NoSuchGroupException, OrmException, IOException {
     seen.add(AccountGroup.PROJECT_OWNERS);
     if (project == null) {
       return Collections.emptySet();
@@ -100,7 +101,7 @@
 
   private Set<Account> getGroupMembers(final AccountGroup group,
       final Project.NameKey project, final Set<AccountGroup.UUID> seen)
-      throws NoSuchGroupException, OrmException, NoSuchProjectException {
+      throws NoSuchGroupException, OrmException, NoSuchProjectException, IOException {
     seen.add(group.getGroupUUID());
     final GroupDetail groupDetail =
         groupDetailFactory.create(group.getId()).call();
@@ -112,7 +113,7 @@
       }
     }
     if (groupDetail.includes != null) {
-      for (final AccountGroupIncludeByUuid groupInclude : groupDetail.includes) {
+      for (final AccountGroupById groupInclude : groupDetail.includes) {
         final AccountGroup includedGroup =
             groupCache.get(groupInclude.getIncludeUUID());
         if (includedGroup != null && !seen.contains(includedGroup.getGroupUUID())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
new file mode 100644
index 0000000..807fa1f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -0,0 +1,119 @@
+// 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.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountInfo.AvatarInfo;
+import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.Set;
+
+@Singleton
+public class InternalAccountDirectory extends AccountDirectory {
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(AccountDirectory.class).to(InternalAccountDirectory.class);
+    }
+  }
+
+  private final Provider<ReviewDb> db;
+  private final AccountCache accountCache;
+  private final DynamicItem<AvatarProvider> avatar;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  InternalAccountDirectory(Provider<ReviewDb> db,
+      AccountCache accountCache,
+      DynamicItem<AvatarProvider> avatar,
+      IdentifiedUser.GenericFactory userFactory) {
+    this.db = db;
+    this.accountCache = accountCache;
+    this.avatar = avatar;
+    this.userFactory = userFactory;
+  }
+
+  @Override
+  public void fillAccountInfo(
+      Iterable<? extends AccountInfo> in,
+      Set<FillOptions> options)
+      throws DirectoryException {
+    Multimap<Account.Id, AccountInfo> missing = ArrayListMultimap.create();
+    for (AccountInfo info : in) {
+      AccountState state = accountCache.getIfPresent(info._id);
+      if (state != null) {
+        fill(info, state.getAccount(), options);
+      } else {
+        missing.put(info._id, info);
+      }
+    }
+    if (!missing.isEmpty()) {
+      try {
+        for (Account account : db.get().accounts().get(missing.keySet())) {
+          for (AccountInfo info : missing.get(account.getId())) {
+            fill(info, account, options);
+          }
+        }
+      } catch (OrmException e) {
+        throw new DirectoryException(e);
+      }
+    }
+  }
+
+  private void fill(AccountInfo info,
+      Account account,
+      Set<FillOptions> options) {
+    if (options.contains(FillOptions.NAME)) {
+      info.name = Strings.emptyToNull(account.getFullName());
+      if (info.name == null) {
+        info.name = account.getUserName();
+      }
+    }
+    if (options.contains(FillOptions.EMAIL)) {
+      info.email = account.getPreferredEmail();
+    }
+    if (options.contains(FillOptions.USERNAME)) {
+      info.username = account.getUserName();
+    }
+    if (options.contains(FillOptions.AVATARS)) {
+      info.avatars = Lists.newArrayListWithCapacity(1);
+      AvatarProvider ap = avatar.get();
+      if (ap != null) {
+        String u = ap.getUrl(
+            userFactory.create(account.getId()),
+            AvatarInfo.DEFAULT_SIZE);
+        if (u != null) {
+          AvatarInfo a = new AvatarInfo();
+          a.url = u;
+          a.height = AvatarInfo.DEFAULT_SIZE;
+          info.avatars.add(a);
+        }
+      }
+    }
+  }
+}
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 57a4a22..11f2e91 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
@@ -16,9 +16,13 @@
 
 import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
 import static com.google.gerrit.server.account.AccountResource.CAPABILITY_KIND;
+import static com.google.gerrit.server.account.AccountResource.EMAIL_KIND;
+import static com.google.gerrit.server.account.AccountResource.SSH_KEY_KIND;
+import static com.google.gerrit.server.account.AccountResource.STARRED_CHANGE_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class Module extends RestApiModule {
   @Override
@@ -28,14 +32,47 @@
 
     DynamicMap.mapOf(binder(), ACCOUNT_KIND);
     DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+    DynamicMap.mapOf(binder(), EMAIL_KIND);
+    DynamicMap.mapOf(binder(), SSH_KEY_KIND);
+    DynamicMap.mapOf(binder(), STARRED_CHANGE_KIND);
 
+    put(ACCOUNT_KIND).to(PutAccount.class);
     get(ACCOUNT_KIND).to(GetAccount.class);
+    get(ACCOUNT_KIND, "name").to(GetName.class);
+    put(ACCOUNT_KIND, "name").to(PutName.class);
+    delete(ACCOUNT_KIND, "name").to(PutName.class);
+    get(ACCOUNT_KIND, "username").to(GetUsername.class);
+    get(ACCOUNT_KIND, "active").to(GetActive.class);
+    put(ACCOUNT_KIND, "active").to(PutActive.class);
+    delete(ACCOUNT_KIND, "active").to(DeleteActive.class);
+    child(ACCOUNT_KIND, "emails").to(Emails.class);
+    get(EMAIL_KIND).to(GetEmail.class);
+    put(EMAIL_KIND).to(PutEmail.class);
+    delete(EMAIL_KIND).to(DeleteEmail.class);
+    put(EMAIL_KIND, "preferred").to(PutPreferred.class);
+    get(ACCOUNT_KIND, "password.http").to(GetHttpPassword.class);
+    put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
+    delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
+    child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
+    post(ACCOUNT_KIND, "sshkeys").to(AddSshKey.class);
+    get(SSH_KEY_KIND).to(GetSshKey.class);
+    delete(SSH_KEY_KIND).to(DeleteSshKey.class);
     get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
     get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
     child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
     get(ACCOUNT_KIND, "groups").to(GetGroups.class);
+    get(ACCOUNT_KIND, "preferences").to(GetPreferences.class);
+    post(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
     get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
     put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
+
+    child(ACCOUNT_KIND, "starred.changes").to(StarredChanges.class);
+    put(STARRED_CHANGE_KIND).to(StarredChanges.Put.class);
+    delete(STARRED_CHANGE_KIND).to(StarredChanges.Delete.class);
+    bind(StarredChanges.Create.class);
+
+    install(new FactoryModuleBuilder().build(CreateAccount.Factory.class));
+    install(new FactoryModuleBuilder().build(CreateEmail.Factory.class));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index 8b966cb..92e9b86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -18,14 +18,15 @@
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -147,8 +148,8 @@
           new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId));
       memberships.add(membership);
 
-      final AccountGroupMemberAudit audit =
-          new AccountGroupMemberAudit(membership, currentUser.getAccountId());
+      final AccountGroupMemberAudit audit = new AccountGroupMemberAudit(
+          membership, currentUser.getAccountId(), TimeUtil.nowTs());
       membershipsAudit.add(audit);
     }
     db.accountGroupMembers().insert(memberships);
@@ -161,21 +162,21 @@
 
   private void addGroups(final AccountGroup.Id groupId,
       final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
-    final List<AccountGroupIncludeByUuid> includeList =
-      new ArrayList<AccountGroupIncludeByUuid>();
-    final List<AccountGroupIncludeByUuidAudit> includesAudit =
-      new ArrayList<AccountGroupIncludeByUuidAudit>();
+    final List<AccountGroupById> includeList =
+      new ArrayList<AccountGroupById>();
+    final List<AccountGroupByIdAud> includesAudit =
+      new ArrayList<AccountGroupByIdAud>();
     for (AccountGroup.UUID includeUUID : groups) {
-      final AccountGroupIncludeByUuid groupInclude =
-        new AccountGroupIncludeByUuid(new AccountGroupIncludeByUuid.Key(groupId, includeUUID));
+      final AccountGroupById groupInclude =
+        new AccountGroupById(new AccountGroupById.Key(groupId, includeUUID));
       includeList.add(groupInclude);
 
-      final AccountGroupIncludeByUuidAudit audit =
-        new AccountGroupIncludeByUuidAudit(groupInclude, currentUser.getAccountId());
+      final AccountGroupByIdAud audit = new AccountGroupByIdAud(
+          groupInclude, currentUser.getAccountId(), TimeUtil.nowTs());
       includesAudit.add(audit);
     }
-    db.accountGroupIncludesByUuid().insert(includeList);
-    db.accountGroupIncludesByUuidAudit().insert(includesAudit);
+    db.accountGroupById().insert(includeList);
+    db.accountGroupByIdAud().insert(includesAudit);
 
     for (AccountGroup.UUID uuid : groups) {
       groupIncludeCache.evictMemberIn(uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
new file mode 100644
index 0000000..f7584ed
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
@@ -0,0 +1,27 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.CreateAccount.Input;
+
+public class PutAccount implements RestModifyView<AccountResource, Input> {
+  @Override
+  public Object apply(AccountResource resource, Input input)
+      throws ResourceConflictException {
+    throw new ResourceConflictException("account exists");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
new file mode 100644
index 0000000..a860fda
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -0,0 +1,60 @@
+// 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.account;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+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.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.PutActive.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class PutActive implements RestModifyView<AccountResource, Input> {
+  public static class Input {
+  }
+
+  private final Provider<ReviewDb> dbProvider;
+  private final AccountCache byIdCache;
+
+  @Inject
+  PutActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+    this.dbProvider = dbProvider;
+    this.byIdCache = byIdCache;
+  }
+
+  @Override
+  public Object apply(AccountResource rsrc, Input input)
+      throws ResourceNotFoundException, OrmException {
+    Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+    if (a == null) {
+      throw new ResourceNotFoundException("account not found");
+    }
+    if (a.isActive()) {
+      return Response.ok("");
+    }
+    a.setActive(true);
+    dbProvider.get().accounts().update(Collections.singleton(a));
+    byIdCache.evict(a.getId());
+    return Response.created("");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
new file mode 100644
index 0000000..b79d8dc4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
@@ -0,0 +1,27 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.CreateEmail.Input;
+
+public class PutEmail implements RestModifyView<AccountResource.Email, Input> {
+  @Override
+  public Object apply(AccountResource.Email rsrc, Input input)
+      throws ResourceConflictException {
+    throw new ResourceConflictException("email exists");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
new file mode 100644
index 0000000..0601b8d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -0,0 +1,132 @@
+// 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.account;
+
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
+
+import com.google.common.base.Strings;
+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.RestModifyView;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.PutHttpPassword.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Collections;
+
+public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
+  public static class Input {
+    public String httpPassword;
+    public boolean generate;
+  }
+
+  private static final int LEN = 12;
+  private static final SecureRandom rng;
+
+  static {
+    try {
+      rng = SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("Cannot create RNG for password generator", e);
+    }
+  }
+
+  private final Provider<CurrentUser> self;
+  private final Provider<ReviewDb> dbProvider;
+  private final AccountCache accountCache;
+
+  @Inject
+  PutHttpPassword(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
+      AccountCache accountCache) {
+    this.self = self;
+    this.dbProvider = dbProvider;
+    this.accountCache = accountCache;
+  }
+
+  @Override
+  public Response<String> apply(AccountResource rsrc, Input input) throws AuthException,
+      ResourceNotFoundException, ResourceConflictException, OrmException {
+    if (input == null) {
+      input = new Input();
+    }
+    input.httpPassword = Strings.emptyToNull(input.httpPassword);
+
+    String newPassword;
+    if (input.generate) {
+      if (self.get() != rsrc.getUser()
+          && !self.get().getCapabilities().canGenerateHttpPassword()) {
+        throw new AuthException("not allowed to generate HTTP password");
+      }
+      newPassword = generate();
+
+    } else if (input.httpPassword == null) {
+      if (self.get() != rsrc.getUser()
+          && !self.get().getCapabilities().canAdministrateServer()) {
+        throw new AuthException("not allowed to clear HTTP password");
+      }
+      newPassword = null;
+    } else {
+      if (!self.get().getCapabilities().canAdministrateServer()) {
+        throw new AuthException("not allowed to set HTTP password directly, "
+            + "need to be Gerrit administrator");
+      }
+      newPassword = input.httpPassword;
+    }
+
+    if (rsrc.getUser().getUserName() == null) {
+      throw new ResourceConflictException("username must be set");
+    }
+
+    AccountExternalId id = dbProvider.get().accountExternalIds()
+        .get(new AccountExternalId.Key(
+            SCHEME_USERNAME,
+            rsrc.getUser().getUserName()));
+    if (id == null) {
+      throw new ResourceNotFoundException();
+    }
+    id.setPassword(newPassword);
+    dbProvider.get().accountExternalIds().update(Collections.singleton(id));
+    accountCache.evict(rsrc.getUser().getAccountId());
+
+    return Strings.isNullOrEmpty(newPassword)
+        ? Response.<String>none()
+        : Response.ok(newPassword);
+  }
+
+  private static String generate() {
+    byte[] rand = new byte[LEN];
+    rng.nextBytes(rand);
+
+    byte[] enc = Base64.encodeBase64(rand, false);
+    StringBuilder r = new StringBuilder(LEN);
+    for (int i = 0; i < LEN; i++) {
+      if (enc[i] == '=') {
+        break;
+      }
+      r.append((char) enc[i]);
+    }
+    return r.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
new file mode 100644
index 0000000..50497c7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -0,0 +1,83 @@
+// 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.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.PutName.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class PutName implements RestModifyView<AccountResource, Input> {
+  public static class Input {
+    @DefaultInput
+    public String name;
+  }
+
+  private final Provider<CurrentUser> self;
+  private final Realm realm;
+  private final Provider<ReviewDb> dbProvider;
+  private final AccountCache byIdCache;
+
+  @Inject
+  PutName(Provider<CurrentUser> self, Realm realm,
+      Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+    this.self = self;
+    this.realm = realm;
+    this.dbProvider = dbProvider;
+    this.byIdCache = byIdCache;
+  }
+
+  @Override
+  public Response<String> apply(AccountResource rsrc, Input input)
+      throws AuthException, MethodNotAllowedException,
+      ResourceNotFoundException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to change name");
+    }
+
+    if (!realm.allowsEdit(FieldName.FULL_NAME)) {
+      throw new MethodNotAllowedException("realm does not allow editing name");
+    }
+
+    if (input == null) {
+      input = new Input();
+    }
+
+    Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+    if (a == null) {
+      throw new ResourceNotFoundException("account not found");
+    }
+    a.setFullName(input.name);
+    dbProvider.get().accounts().update(Collections.singleton(a));
+    byIdCache.evict(a.getId());
+    return Strings.isNullOrEmpty(a.getFullName())
+        ? Response.<String> none()
+        : Response.ok(a.getFullName());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
new file mode 100644
index 0000000..d81f361
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -0,0 +1,68 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+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.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.PutPreferred.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class PutPreferred implements
+    RestModifyView<AccountResource.Email, Input> {
+  static class Input {
+  }
+
+  private final Provider<CurrentUser> self;
+  private final Provider<ReviewDb> dbProvider;
+  private final AccountCache byIdCache;
+
+  @Inject
+  PutPreferred(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
+      AccountCache byIdCache) {
+    this.self = self;
+    this.dbProvider = dbProvider;
+    this.byIdCache = byIdCache;
+  }
+
+  @Override
+  public Response<String> apply(AccountResource.Email rsrc, Input input)
+      throws AuthException, ResourceNotFoundException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to set preferred email address");
+    }
+
+    Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+    if (a == null) {
+      throw new ResourceNotFoundException("account not found");
+    }
+    if (rsrc.getEmail().equals(a.getPreferredEmail())) {
+      return Response.ok("");
+    }
+    a.setPreferredEmail(rsrc.getEmail());
+    dbProvider.get().accounts().update(Collections.singleton(a));
+    byIdCache.evict(a.getId());
+    return Response.created("");
+  }
+}
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
new file mode 100644
index 0000000..8e40b2e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -0,0 +1,139 @@
+// 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.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.SetPreferences.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class SetPreferences implements RestModifyView<AccountResource, Input> {
+  static class Input {
+    Short changesPerPage;
+    Boolean showSiteHeader;
+    Boolean useFlashClipboard;
+    DownloadScheme downloadScheme;
+    DownloadCommand downloadCommand;
+    Boolean copySelfOnEmail;
+    DateFormat dateFormat;
+    TimeFormat timeFormat;
+    Boolean reversePatchSetOrder;
+    Boolean showUsernameInReviewCategory;
+    Boolean relativeDateInChangeTable;
+    CommentVisibilityStrategy commentVisibilityStrategy;
+    DiffView diffView;
+  }
+
+  private final Provider<CurrentUser> self;
+  private final AccountCache cache;
+  private final ReviewDb db;
+
+  @Inject
+  SetPreferences(Provider<CurrentUser> self, AccountCache cache, ReviewDb db) {
+    this.self = self;
+    this.cache = cache;
+    this.db = db;
+  }
+
+  @Override
+  public GetPreferences.PreferenceInfo apply(AccountResource rsrc, Input i)
+      throws AuthException, ResourceNotFoundException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+    if (i == null) {
+      i = new Input();
+    }
+
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    AccountGeneralPreferences p;
+    db.accounts().beginTransaction(accountId);
+    try {
+      Account a = db.accounts().get(accountId);
+      if (a == null) {
+        throw new ResourceNotFoundException();
+      }
+
+      p = a.getGeneralPreferences();
+      if (p == null) {
+        p = new AccountGeneralPreferences();
+        a.setGeneralPreferences(p);
+      }
+
+      if (i.changesPerPage != null) {
+        p.setMaximumPageSize(i.changesPerPage);
+      }
+      if (i.showSiteHeader != null) {
+        p.setShowSiteHeader(i.showSiteHeader);
+      }
+      if (i.useFlashClipboard != null) {
+        p.setUseFlashClipboard(i.useFlashClipboard);
+      }
+      if (i.downloadScheme != null) {
+        p.setDownloadUrl(i.downloadScheme);
+      }
+      if (i.downloadCommand != null) {
+        p.setDownloadCommand(i.downloadCommand);
+      }
+      if (i.copySelfOnEmail != null) {
+        p.setCopySelfOnEmails(i.copySelfOnEmail);
+      }
+      if (i.dateFormat != null) {
+        p.setDateFormat(i.dateFormat);
+      }
+      if (i.timeFormat != null) {
+        p.setTimeFormat(i.timeFormat);
+      }
+      if (i.reversePatchSetOrder != null) {
+        p.setReversePatchSetOrder(i.reversePatchSetOrder);
+      }
+      if (i.showUsernameInReviewCategory != null) {
+        p.setShowUsernameInReviewCategory(i.showUsernameInReviewCategory);
+      }
+      if (i.relativeDateInChangeTable != null) {
+        p.setRelativeDateInChangeTable(i.relativeDateInChangeTable);
+      }
+      if (i.commentVisibilityStrategy != null) {
+        p.setCommentVisibilityStrategy(i.commentVisibilityStrategy);
+      }
+      if (i.diffView != null) {
+        p.setDiffView(i.diffView);
+      }
+
+      db.accounts().update(Collections.singleton(a));
+      db.commit();
+      cache.evict(accountId);
+    } finally {
+      db.rollback();
+    }
+    return new GetPreferences.PreferenceInfo(p);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
new file mode 100644
index 0000000..5578c3f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -0,0 +1,77 @@
+// 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.account;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class SshKeys implements
+    ChildCollection<AccountResource, AccountResource.SshKey> {
+  private final DynamicMap<RestView<AccountResource.SshKey>> views;
+  private final Provider<GetSshKeys> list;
+  private final Provider<CurrentUser> self;
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  SshKeys(DynamicMap<RestView<AccountResource.SshKey>> views,
+      Provider<GetSshKeys> list, Provider<CurrentUser> self,
+      Provider<ReviewDb> dbProvider) {
+    this.views = views;
+    this.list = list;
+    this.self = self;
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public RestView<AccountResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
+      throws ResourceNotFoundException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new ResourceNotFoundException();
+    }
+
+    try {
+      int seq = Integer.parseInt(id.get(), 10);
+      AccountSshKey sshKey =
+          dbProvider.get().accountSshKeys()
+              .get(new AccountSshKey.Id(rsrc.getUser().getAccountId(), seq));
+      if (sshKey == null) {
+        throw new ResourceNotFoundException(id);
+      }
+      return new AccountResource.SshKey(rsrc.getUser(), sshKey);
+    } catch (NumberFormatException e) {
+      throw new ResourceNotFoundException(id);
+    }
+  }
+
+  @Override
+  public DynamicMap<RestView<AccountResource.SshKey>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
new file mode 100644
index 0000000..0e335d0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
@@ -0,0 +1,199 @@
+// 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.account;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.StarredChange;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.query.change.QueryChanges;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+
+class StarredChanges implements
+    ChildCollection<AccountResource, AccountResource.StarredChange>,
+    AcceptsCreate<AccountResource> {
+  private static final Logger log = LoggerFactory.getLogger(StarredChanges.class);
+
+  private final ChangesCollection changes;
+  private final DynamicMap<RestView<AccountResource.StarredChange>> views;
+  private final Provider<Create> createProvider;
+
+  @Inject
+  StarredChanges(ChangesCollection changes,
+      DynamicMap<RestView<AccountResource.StarredChange>> views,
+      Provider<Create> createProvider) {
+    this.changes = changes;
+    this.views = views;
+    this.createProvider = createProvider;
+  }
+
+  @Override
+  public AccountResource.StarredChange parse(AccountResource parent, IdString id)
+      throws ResourceNotFoundException, OrmException, UnsupportedEncodingException {
+    IdentifiedUser user = parent.getUser();
+    try {
+      user.asyncStarredChanges();
+
+      ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
+      if (user.getStarredChanges().contains(change.getChange().getId())) {
+        return new AccountResource.StarredChange(user, change);
+      }
+      throw new ResourceNotFoundException(id);
+    } finally {
+      user.abortStarredChanges();
+    }
+  }
+
+  @Override
+  public DynamicMap<RestView<AccountResource.StarredChange>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<AccountResource> list() throws ResourceNotFoundException {
+    return new RestReadView<AccountResource>() {
+      @Override
+      public Object apply(AccountResource self) throws BadRequestException,
+          AuthException, OrmException {
+        QueryChanges query = changes.list();
+        query.addQuery("starredby:" + self.getUser().getAccountId().get());
+        return query.apply(TopLevelResource.INSTANCE);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public RestModifyView<AccountResource, EmptyInput> create(
+      AccountResource parent, IdString id) throws UnprocessableEntityException{
+    try {
+      return createProvider.get()
+          .setChange(changes.parse(TopLevelResource.INSTANCE, id));
+    } catch (ResourceNotFoundException e) {
+      throw new UnprocessableEntityException(String.format("change %s not found", id.get()));
+    } catch (UnsupportedEncodingException e) {
+      log.error("cannot resolve change", e);
+      throw new UnprocessableEntityException("internal server error");
+    } catch (OrmException e) {
+      log.error("cannot resolve change", e);
+      throw new UnprocessableEntityException("internal server error");
+    }
+  }
+
+  static class Create implements RestModifyView<AccountResource, EmptyInput> {
+    private final Provider<CurrentUser> self;
+    private final Provider<ReviewDb> dbProvider;
+    private ChangeResource change;
+
+    @Inject
+    Create(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+      this.self = self;
+      this.dbProvider = dbProvider;
+    }
+
+    Create setChange(ChangeResource change) {
+      this.change = change;
+      return this;
+    }
+
+    @Override
+    public Response<?> apply(AccountResource rsrc, EmptyInput in)
+        throws AuthException, OrmException {
+      if (self.get() != rsrc.getUser()) {
+        throw new AuthException("not allowed to add starred change");
+      }
+      try {
+        dbProvider.get().starredChanges().insert(Collections.singleton(
+            new StarredChange(new StarredChange.Key(
+                rsrc.getUser().getAccountId(),
+                change.getChange().getId()))));
+      } catch (OrmDuplicateKeyException e) {
+        return Response.none();
+      }
+      return Response.none();
+    }
+  }
+
+  static class Put implements
+      RestModifyView<AccountResource.StarredChange, EmptyInput> {
+    private final Provider<CurrentUser> self;
+
+    @Inject
+    Put(Provider<CurrentUser> self) {
+      this.self = self;
+    }
+
+    @Override
+    public Response<?> apply(AccountResource.StarredChange rsrc, EmptyInput in)
+        throws AuthException, OrmException {
+      if (self.get() != rsrc.getUser()) {
+        throw new AuthException("not allowed update starred changes");
+      }
+      return Response.none();
+    }
+  }
+
+  static class Delete implements
+      RestModifyView<AccountResource.StarredChange, EmptyInput> {
+    private final Provider<CurrentUser> self;
+    private final Provider<ReviewDb> dbProvider;
+
+    @Inject
+    Delete(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+      this.self = self;
+      this.dbProvider = dbProvider;
+    }
+
+    @Override
+    public Response<?> apply(AccountResource.StarredChange rsrc,
+        EmptyInput in) throws AuthException, OrmException {
+      if (self.get() != rsrc.getUser()) {
+        throw new AuthException("not allowed remove starred change");
+      }
+      dbProvider.get().starredChanges().delete(Collections.singleton(
+          new StarredChange(new StarredChange.Key(
+              rsrc.getUser().getAccountId(),
+              rsrc.getChange().getId()))));
+      return Response.none();
+    }
+  }
+
+  static class EmptyInput {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 046dfa5..1748395d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -37,8 +38,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import javax.annotation.Nullable;
-
 /**
  * Universal implementation of the GroupBackend that works with the injected
  * set of GroupBackends.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/actions/ActionInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/actions/ActionInfo.java
new file mode 100644
index 0000000..91574a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/actions/ActionInfo.java
@@ -0,0 +1,31 @@
+// 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.actions;
+
+import com.google.gerrit.extensions.webui.UiAction;
+
+public class ActionInfo {
+  String method;
+  String label;
+  String title;
+  Boolean enabled;
+
+  public ActionInfo(UiAction.Description d) {
+    method = d.getMethod();
+    label = d.getLabel();
+    title = d.getTitle();
+    enabled = d.isEnabled() ? true : null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
index 7b44c02..8771c23d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
@@ -16,9 +16,11 @@
 
 import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.kohsuke.args4j.CmdLineException;
@@ -27,17 +29,26 @@
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Parameters;
 import org.kohsuke.args4j.spi.Setter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
 
 public class ProjectControlHandler extends OptionHandler<ProjectControl> {
-  private final ProjectControl.Factory projectControlFactory;
+  private static final Logger log = LoggerFactory
+      .getLogger(ProjectControlHandler.class);
+  private final ProjectControl.GenericFactory projectControlFactory;
+  private final Provider<CurrentUser> user;
 
   @Inject
   public ProjectControlHandler(
-      final ProjectControl.Factory projectControlFactory,
+      final ProjectControl.GenericFactory projectControlFactory,
+      Provider<CurrentUser> user,
       @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
       @Assisted final Setter<ProjectControl> setter) {
     super(parser, option, setter);
     this.projectControlFactory = projectControlFactory;
+    this.user = user;
   }
 
   @Override
@@ -59,13 +70,21 @@
     }
 
     String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
+    Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix);
 
     final ProjectControl control;
     try {
-      Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix);
-      control = projectControlFactory.validateFor(nameKey, ProjectControl.OWNER | ProjectControl.VISIBLE);
+      control = projectControlFactory.validateFor(
+          nameKey,
+          ProjectControl.OWNER | ProjectControl.VISIBLE,
+          user.get());
     } catch (NoSuchProjectException e) {
       throw new CmdLineException(owner, e.getMessage());
+    } catch (IOException e) {
+      log.warn("Cannot load project " + nameWithoutSuffix, e);
+      throw new CmdLineException(
+          owner,
+          new NoSuchProjectException(nameKey).getMessage());
     }
 
     setter.addValue(control);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
index 8179d4f..09ab56b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
@@ -15,8 +15,7 @@
 package com.google.gerrit.server.auth;
 
 import com.google.common.base.Objects;
-
-import javax.annotation.Nullable;
+import com.google.gerrit.common.Nullable;
 
 /**
  * Defines an abstract request for user authentication to Gerrit.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
index 7d08173..65f1f58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import javax.annotation.Nullable;
+import com.google.gerrit.common.Nullable;
 
 /**
  * An authenticated user as specified by the AuthBackend.
@@ -26,7 +26,7 @@
   /**
    * Globally unique identifier for the user.
    */
-  public final static class UUID {
+  public static final class UUID {
     private final String uuid;
 
     /**
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 97a0309..26c1c7a 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
@@ -21,6 +21,7 @@
 
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.ParameterizedString;
@@ -48,7 +49,6 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
-import javax.annotation.Nullable;
 import javax.naming.InvalidNameException;
 import javax.naming.NamingException;
 import javax.naming.directory.DirContext;
@@ -126,7 +126,7 @@
 
     String groupDn = uuid.get().substring(LDAP_UUID.length());
     CurrentUser user = userProvider.get();
-    if (!(user instanceof IdentifiedUser)
+    if (!(user.isIdentifiedUser())
         || !membershipsOf((IdentifiedUser) user).contains(uuid)) {
       try {
         if (!existsCache.get(groupDn)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
index c0140d0..4b30a8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
@@ -42,7 +42,7 @@
   /**
    * Gets a URL for a user to modify their avatar image.
    *
-   * @param user The user wishing to change their avatar image
+   * @param forUser The user wishing to change their avatar image
    * @return a URL the user should visit to modify their avatar, or null if
    *         modification is not possible.
    */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
index 625bd14..7062871 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -16,12 +16,11 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.Weigher;
+import com.google.gerrit.common.Nullable;
 import com.google.inject.TypeLiteral;
 
 import java.util.concurrent.TimeUnit;
 
-import javax.annotation.Nullable;
-
 /** Configure a cache declared within a {@link CacheModule} instance. */
 public interface CacheBinding<K, V> {
   /** Set the total size of the cache. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index c1e92da..c093380 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -85,7 +85,7 @@
 
     CacheProvider<K, V> m =
         new CacheProvider<K, V>(this, name, keyType, valType);
-    bind(key).toProvider(m).in(Scopes.SINGLETON);
+    bind(key).toProvider(m).asEagerSingleton();
     bind(ANY_CACHE).annotatedWith(Exports.named(name)).to(key);
     return m.maximumWeight(1024);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
index 1b8eea5..6d9ae0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -21,6 +21,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.Weigher;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -28,8 +29,6 @@
 
 import java.util.concurrent.TimeUnit;
 
-import javax.annotation.Nullable;
-
 class CacheProvider<K, V>
     implements Provider<Cache<K, V>>,
     CacheBinding<K, V> {
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 f766297..4959cfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -28,6 +30,8 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.Abandon.Input;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.project.ChangeControl;
@@ -39,15 +43,18 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Collections;
 
-public class Abandon implements RestModifyView<ChangeResource, Input> {
+public class Abandon implements RestModifyView<ChangeResource, Input>,
+    UiAction<ChangeResource> {
   private static final Logger log = LoggerFactory.getLogger(Abandon.class);
 
   private final ChangeHooks hooks;
   private final AbandonedSender.Factory abandonedSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
+  private final ChangeIndexer indexer;
 
   public static class Input {
     @DefaultInput
@@ -58,11 +65,13 @@
   Abandon(ChangeHooks hooks,
       AbandonedSender.Factory abandonedSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json) {
+      ChangeJson json,
+      ChangeIndexer indexer) {
     this.hooks = hooks;
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
+    this.indexer = indexer;
   }
 
   @Override
@@ -107,6 +116,7 @@
       db.rollback();
     }
 
+    CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
     try {
       ReplyToChangeSender cm = abandonedSenderFactory.create(change);
       cm.setFrom(caller.getAccountId());
@@ -117,9 +127,21 @@
     }
     hooks.doChangeAbandonedHook(change,
         caller.getAccount(),
+        db.patchSets().get(change.currentPatchSetId()),
         Strings.emptyToNull(input.message),
         db);
-    return json.format(change);
+    ChangeInfo result = json.format(change);
+    indexFuture.checkedGet();
+    return result;
+  }
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource resource) {
+    return new UiAction.Description()
+      .setLabel("Abandon")
+      .setTitle("Abandon the change")
+      .setVisible(resource.getChange().getStatus().isOpen()
+          && resource.getControl().canAbandon());
   }
 
   private ChangeMessage newMessage(Input input, IdentifiedUser caller,
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 5c965e2..77ad83b 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,6 +14,9 @@
 
 package com.google.gerrit.server.change;
 
+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.reviewdb.client.Account;
@@ -21,65 +24,179 @@
 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.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.RefControl;
 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.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.Set;
 
 public class ChangeInserter {
+  public static interface Factory {
+    ChangeInserter create(RefControl ctl, Change c, RevCommit rc);
+  }
+
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeInserter.class);
+
+  private final Provider<ReviewDb> dbProvider;
   private final GitReferenceUpdated gitRefUpdated;
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final TrackingFooters trackingFooters;
+  private final ChangeIndexer indexer;
+  private final CreateChangeSender.Factory createChangeSenderFactory;
+
+  private final RefControl refControl;
+  private final Change change;
+  private final PatchSet patchSet;
+  private final RevCommit commit;
+  private final PatchSetInfo patchSetInfo;
+
+  private ChangeMessage changeMessage;
+  private Set<Account.Id> reviewers;
+  private Set<Account.Id> extraCC;
+  private boolean runHooks;
+  private boolean sendMail;
 
   @Inject
-  public ChangeInserter(final GitReferenceUpdated gitRefUpdated,
-      ChangeHooks hooks, ApprovalsUtil approvalsUtil,
-      TrackingFooters trackingFooters) {
+  ChangeInserter(Provider<ReviewDb> dbProvider,
+      PatchSetInfoFactory patchSetInfoFactory,
+      GitReferenceUpdated gitRefUpdated,
+      ChangeHooks hooks,
+      ApprovalsUtil approvalsUtil,
+      TrackingFooters trackingFooters,
+      ChangeIndexer indexer,
+      CreateChangeSender.Factory createChangeSenderFactory,
+      @Assisted RefControl refControl,
+      @Assisted Change change,
+      @Assisted RevCommit commit) {
+    this.dbProvider = dbProvider;
     this.gitRefUpdated = gitRefUpdated;
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.trackingFooters = trackingFooters;
+    this.indexer = indexer;
+    this.createChangeSenderFactory = createChangeSenderFactory;
+    this.refControl = refControl;
+    this.change = change;
+    this.commit = commit;
+    this.reviewers = Collections.emptySet();
+    this.extraCC = Collections.emptySet();
+    this.runHooks = true;
+    this.sendMail = true;
+
+    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);
+    ChangeUtil.computeSortKey(change);
   }
 
-  public void insertChange(ReviewDb db, Change change, PatchSet ps,
-      RevCommit commit, LabelTypes labelTypes, PatchSetInfo info,
-      Set<Account.Id> reviewers) throws OrmException {
-    insertChange(db, change, null, ps, commit, labelTypes, info, reviewers);
+  public Change getChange() {
+    return change;
   }
 
-  public void insertChange(ReviewDb db, Change change,
-      ChangeMessage changeMessage, PatchSet ps, RevCommit commit,
-      LabelTypes labelTypes, PatchSetInfo info, Set<Account.Id> reviewers)
-      throws OrmException {
+  public ChangeInserter setMessage(ChangeMessage changeMessage) {
+    this.changeMessage = changeMessage;
+    return this;
+  }
 
+  public ChangeInserter setReviewers(Set<Account.Id> reviewers) {
+    this.reviewers = reviewers;
+    return this;
+  }
+
+  public ChangeInserter setExtraCC(Set<Account.Id> extraCC) {
+    this.extraCC = extraCC;
+    return this;
+  }
+
+  public ChangeInserter setDraft(boolean draft) {
+    change.setStatus(draft ? Change.Status.DRAFT : Change.Status.NEW);
+    patchSet.setDraft(draft);
+    return this;
+  }
+
+  public ChangeInserter setRunHooks(boolean runHooks) {
+    this.runHooks = runHooks;
+    return this;
+  }
+
+  public ChangeInserter setSendMail(boolean sendMail) {
+    this.sendMail = sendMail;
+    return this;
+  }
+
+  public PatchSet getPatchSet() {
+    return patchSet;
+  }
+
+  public PatchSetInfo getPatchSetInfo() {
+    return patchSetInfo;
+  }
+
+  public Change insert() throws OrmException, IOException {
+    ReviewDb db = dbProvider.get();
     db.changes().beginTransaction(change.getId());
     try {
-      ChangeUtil.insertAncestors(db, ps.getId(), commit);
-      db.patchSets().insert(Collections.singleton(ps));
+      ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+      db.patchSets().insert(Collections.singleton(patchSet));
       db.changes().insert(Collections.singleton(change));
       ChangeUtil.updateTrackingIds(db, change, trackingFooters, commit.getFooterLines());
-      approvalsUtil.addReviewers(db, labelTypes, change, ps, info, reviewers,
-          Collections.<Account.Id> emptySet());
-      if (changeMessage != null) {
-        db.changeMessages().insert(Collections.singleton(changeMessage));
-      }
+      LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
+      approvalsUtil.addReviewers(db, labelTypes, change, patchSet, patchSetInfo,
+          reviewers, Collections.<Account.Id> emptySet());
       db.commit();
     } finally {
       db.rollback();
     }
+    if (changeMessage != null) {
+      db.changeMessages().insert(Collections.singleton(changeMessage));
+    }
 
-    gitRefUpdated.fire(change.getProject(), ps.getRefName(), ObjectId.zeroId(),
-        commit);
-    hooks.doPatchsetCreatedHook(change, ps, db);
+    CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
+    gitRefUpdated.fire(change.getProject(), patchSet.getRefName(),
+        ObjectId.zeroId(), commit);
+
+    if (runHooks) {
+      hooks.doPatchsetCreatedHook(change, patchSet, db);
+    }
+
+    if (sendMail) {
+      try {
+        CreateChangeSender cm =
+            createChangeSenderFactory.create(change);
+        cm.setFrom(change.getOwner());
+        cm.setPatchSet(patchSet, patchSetInfo);
+        cm.addReviewers(reviewers);
+        cm.addExtraCC(extraCC);
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot send email for new change " + change.getId(), err);
+      }
+    }
+    indexFuture.checkedGet();
+    return change;
   }
 }
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 99eb8e4..5a3ab3b 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
@@ -17,20 +17,27 @@
 import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
 import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
 import static com.google.gerrit.common.changes.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_ACTIONS;
 import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_FILES;
 import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_ACCOUNTS;
 import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.common.changes.ListChangesOption.DOWNLOAD_COMMANDS;
+import static com.google.gerrit.common.changes.ListChangesOption.DRAFT_COMMENTS;
 import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
 import static com.google.gerrit.common.changes.ListChangesOption.MESSAGES;
+import static com.google.gerrit.common.changes.ListChangesOption.REVIEWED;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
-import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
@@ -46,7 +53,12 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.Url;
+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;
@@ -55,111 +67,122 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.actions.ActionInfo;
+import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.LabelNormalizer;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 
-import com.jcraft.jsch.HostKey;
-
-import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
 
 public class ChangeJson {
   private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+  private static final ResultSet<ChangeMessage> NO_MESSAGES =
+      new ResultSet<ChangeMessage>() {
+        @Override
+        public Iterator<ChangeMessage> iterator() {
+          return toList().iterator();
+        }
 
-  @Singleton
-  static class Urls {
-    final String git;
-    final String http;
+        @Override
+        public List<ChangeMessage> toList() {
+          return Collections.emptyList();
+        }
 
-    @Inject
-    Urls(@GerritServerConfig Config cfg) {
-      this.git = ensureSlash(cfg.getString("gerrit", null, "canonicalGitUrl"));
-      this.http = ensureSlash(cfg.getString("gerrit", null, "gitHttpUrl"));
-    }
-
-    private static String ensureSlash(String in) {
-      if (in != null && !in.endsWith("/")) {
-        return in + "/";
-      }
-      return in;
-    }
-  }
+        @Override
+        public void close() {
+        }
+      };
 
   private final Provider<ReviewDb> db;
   private final LabelNormalizer labelNormalizer;
-  private final CurrentUser user;
+  private final Provider<CurrentUser> userProvider;
   private final AnonymousUser anonymous;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final ChangeControl.GenericFactory changeControlGenericFactory;
+  private final ProjectControl.GenericFactory projectControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final PatchListCache patchListCache;
+  private final FileInfoJson fileInfoJson;
   private final AccountInfo.Loader.Factory accountLoaderFactory;
-  private final Provider<String> urlProvider;
-  private final Urls urls;
-  private ChangeControl.Factory changeControlUserFactory;
-  private SshInfo sshInfo;
+  private final DynamicMap<DownloadScheme> downloadSchemes;
+  private final DynamicMap<DownloadCommand> downloadCommands;
+  private final DynamicMap<RestView<ChangeResource>> changes;
+  private final Revisions revisions;
+
   private EnumSet<ListChangesOption> options;
   private AccountInfo.Loader accountLoader;
   private ChangeControl lastControl;
+  private Set<Change.Id> reviewed;
+  private LoadingCache<Project.NameKey, ProjectControl> projectControls;
 
   @Inject
   ChangeJson(
       Provider<ReviewDb> db,
       LabelNormalizer ln,
-      CurrentUser u,
+      Provider<CurrentUser> user,
       AnonymousUser au,
       IdentifiedUser.GenericFactory uf,
-      ChangeControl.GenericFactory ccf,
+      ProjectControl.GenericFactory pcf,
       PatchSetInfoFactory psi,
-      PatchListCache plc,
+      FileInfoJson fileInfoJson,
       AccountInfo.Loader.Factory ailf,
-      @CanonicalWebUrl Provider<String> curl,
-      Urls urls) {
+      DynamicMap<DownloadScheme> downloadSchemes,
+      DynamicMap<DownloadCommand> downloadCommands,
+      DynamicMap<RestView<ChangeResource>> changes,
+      Revisions revisions) {
     this.db = db;
     this.labelNormalizer = ln;
-    this.user = u;
+    this.userProvider = user;
     this.anonymous = au;
     this.userFactory = uf;
-    this.changeControlGenericFactory = ccf;
+    this.projectControlFactory = pcf;
     this.patchSetInfoFactory = psi;
-    this.patchListCache = plc;
+    this.fileInfoJson = fileInfoJson;
     this.accountLoaderFactory = ailf;
-    this.urlProvider = curl;
-    this.urls = urls;
+    this.downloadSchemes = downloadSchemes;
+    this.downloadCommands = downloadCommands;
+    this.changes = changes;
+    this.revisions = revisions;
 
     options = EnumSet.noneOf(ListChangesOption.class);
+    projectControls = CacheBuilder.newBuilder()
+      .concurrencyLevel(1)
+      .build(new CacheLoader<Project.NameKey, ProjectControl>() {
+        @Override
+        public ProjectControl load(Project.NameKey key)
+            throws NoSuchProjectException, IOException {
+          return projectControlFactory.controlFor(key, userProvider.get());
+        }
+      });
   }
 
   public ChangeJson addOption(ListChangesOption o) {
@@ -172,16 +195,6 @@
     return this;
   }
 
-  public ChangeJson setSshInfo(SshInfo info) {
-    sshInfo = info;
-    return this;
-  }
-
-  public ChangeJson setChangeControlFactory(ChangeControl.Factory cf) {
-    changeControlUserFactory = cf;
-    return this;
-  }
-
   public ChangeInfo format(ChangeResource rsrc) throws OrmException {
     return format(new ChangeData(rsrc.getControl()));
   }
@@ -208,12 +221,22 @@
   public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in)
       throws OrmException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+    Iterable<ChangeData> all = Iterables.concat(in);
+    ChangeData.ensureChangeLoaded(db, all);
+    if (has(ALL_REVISIONS)) {
+      ChangeData.ensureAllPatchSetsLoaded(db, all);
+    } else {
+      ChangeData.ensureCurrentPatchSetLoaded(db, all);
+    }
+    if (has(REVIEWED)) {
+      ensureReviewedLoaded(all);
+    }
+    ChangeData.ensureCurrentApprovalsLoaded(db, all);
+
     List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
+    Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
     for (List<ChangeData> changes : in) {
-      ChangeData.ensureChangeLoaded(db, changes);
-      ChangeData.ensureCurrentPatchSetLoaded(db, changes);
-      ChangeData.ensureCurrentApprovalsLoaded(db, changes);
-      res.add(toChangeInfo(changes));
+      res.add(toChangeInfo(out, changes));
     }
     accountLoader.fill();
     return res;
@@ -223,11 +246,16 @@
     return options.contains(option);
   }
 
-  private List<ChangeInfo> toChangeInfo(List<ChangeData> changes)
-      throws OrmException {
+  private List<ChangeInfo> toChangeInfo(Map<Change.Id, ChangeInfo> out,
+      List<ChangeData> changes) throws OrmException {
     List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
     for (ChangeData cd : changes) {
-      info.add(toChangeInfo(cd));
+      ChangeInfo i = out.get(cd.getId());
+      if (i == null) {
+        i = toChangeInfo(cd);
+        out.put(cd.getId(), i);
+      }
+      info.add(i);
     }
     return info;
   }
@@ -247,8 +275,12 @@
     out.updated = in.getLastUpdatedOn();
     out._number = in.getId().get();
     out._sortkey = in.getSortKey();
-    out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
-    out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
+    out.starred = userProvider.get().getStarredChanges().contains(in.getId())
+        ? true
+        : null;
+    out.reviewed = in.getStatus().isOpen()
+        && has(REVIEWED)
+        && reviewed.contains(cd.getId()) ? true : null;
     out.labels = labelsFor(cd, has(LABELS), has(DETAILED_LABELS));
 
     Collection<PatchSet.Id> limited = cd.getLimitedPatchSets();
@@ -277,13 +309,22 @@
       }
     }
 
+    if (has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
+      out.actions = Maps.newTreeMap();
+      for (UiAction.Description d : UiActions.from(
+          changes,
+          new ChangeResource(control(cd)),
+          userProvider)) {
+        out.actions.put(d.getId(), new ActionInfo(d));
+      }
+    }
     lastControl = null;
     return out;
   }
 
   private ChangeControl control(ChangeData cd) throws OrmException {
     ChangeControl ctrl = cd.changeControl();
-    if (ctrl != null && ctrl.getCurrentUser() == user) {
+    if (ctrl != null && ctrl.getCurrentUser() == userProvider.get()) {
       return ctrl;
     } else if (lastControl != null
         && cd.getId().equals(lastControl.getChange().getId())) {
@@ -291,12 +332,12 @@
     }
 
     try {
-      if (changeControlUserFactory != null) {
-        ctrl = changeControlUserFactory.controlFor(cd.change(db));
-      } else {
-        ctrl = changeControlGenericFactory.controlFor(cd.change(db), user);
+      Change change = cd.change(db);
+      if (change == null) {
+        return null;
       }
-    } catch (NoSuchChangeException e) {
+      ctrl = projectControls.get(change.getProject()).controlFor(change);
+    } catch (ExecutionException e) {
       return null;
     }
     lastControl = ctrl;
@@ -330,11 +371,6 @@
       return null;
     }
 
-    PatchSet ps = cd.currentPatchSet(db);
-    if (ps == null) {
-      return null;
-    }
-
     LabelTypes labelTypes = ctl.getLabelTypes();
     if (cd.getChange().getStatus().isOpen()) {
       return labelsForOpenChange(cd, labelTypes, standard, detailed);
@@ -471,16 +507,18 @@
           continue;
         }
         Integer value;
+        Timestamp date = null;
         PatchSetApproval psa = current.get(accountId, lt.getName());
         if (psa != null) {
           value = Integer.valueOf(psa.getValue());
+          date = psa.getGranted();
         } else {
           // Either the user cannot vote on this label, or there just wasn't a
           // dummy approval for this label. Explicitly check whether the user
           // can vote on this label.
           value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
         }
-        e.getValue().addApproval(approvalInfo(accountId, value));
+        e.getValue().addApproval(approvalInfo(accountId, value, date));
       }
     }
   }
@@ -524,7 +562,7 @@
 
       if (detailed) {
         for (String name : labels.keySet()) {
-          ApprovalInfo ai = approvalInfo(accountId, 0);
+          ApprovalInfo ai = approvalInfo(accountId, 0, null);
           byLabel.put(name, ai);
           labels.get(name).addApproval(ai);
         }
@@ -539,6 +577,7 @@
         ApprovalInfo info = byLabel.get(type.getName());
         if (info != null) {
           info.value = Integer.valueOf(val);
+          info.date = psa.getGranted();
         }
 
         LabelInfo li = labels.get(type.getName());
@@ -552,9 +591,10 @@
     return labels;
   }
 
-  private ApprovalInfo approvalInfo(Account.Id id, Integer value) {
+  private ApprovalInfo approvalInfo(Account.Id id, Integer value, Timestamp date) {
     ApprovalInfo ai = new ApprovalInfo(id);
     ai.value = value;
+    ai.date = date;
     accountLoader.put(ai);
     return ai;
   }
@@ -675,38 +715,49 @@
     return result;
   }
 
-  private boolean isChangeReviewed(ChangeData cd) throws OrmException {
-    if (user instanceof IdentifiedUser) {
-      PatchSet currentPatchSet = cd.currentPatchSet(db);
-      if (currentPatchSet == null) {
-        return false;
-      }
-
-      List<ChangeMessage> messages =
-          db.get().changeMessages().byPatchSet(currentPatchSet.getId()).toList();
-
-      if (messages.isEmpty()) {
-        return false;
-      }
-
-      // Sort messages to let the most recent ones at the beginning.
-      Collections.sort(messages, new Comparator<ChangeMessage>() {
-        @Override
-        public int compare(ChangeMessage a, ChangeMessage b) {
-          return b.getWrittenOn().compareTo(a.getWrittenOn());
+  private void ensureReviewedLoaded(Iterable<ChangeData> all)
+      throws OrmException {
+    reviewed = Sets.newHashSet();
+    if (userProvider.get().isIdentifiedUser()) {
+      Account.Id self = ((IdentifiedUser) userProvider.get()).getAccountId();
+      for (List<ChangeData> batch : Iterables.partition(all, 50)) {
+        List<ResultSet<ChangeMessage>> m =
+            Lists.newArrayListWithCapacity(batch.size());
+        for (ChangeData cd : batch) {
+          PatchSet.Id ps = cd.change(db).currentPatchSetId();
+          if (ps != null && cd.change(db).getStatus().isOpen()) {
+            m.add(db.get().changeMessages().byPatchSet(ps));
+          } else {
+            m.add(NO_MESSAGES);
+          }
         }
-      });
-
-      Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
-      Account.Id changeOwnerId = cd.change(db).getOwner();
-      for (ChangeMessage cm : messages) {
-        if (currentUserId.equals(cm.getAuthor())) {
-          return true;
-        } else if (changeOwnerId.equals(cm.getAuthor())) {
-          return false;
+        for (int i = 0; i < m.size(); i++) {
+          if (isChangeReviewed(self, batch.get(i), m.get(i).toList())) {
+            reviewed.add(batch.get(i).getId());
+          }
         }
       }
     }
+  }
+
+  private boolean isChangeReviewed(Account.Id self, ChangeData cd,
+      List<ChangeMessage> msgs) throws OrmException {
+    // Sort messages to keep the most recent ones at the beginning.
+    Collections.sort(msgs, new Comparator<ChangeMessage>() {
+      @Override
+      public int compare(ChangeMessage a, ChangeMessage b) {
+        return b.getWrittenOn().compareTo(a.getWrittenOn());
+      }
+    });
+
+    Account.Id changeOwnerId = cd.change(db).getOwner();
+    for (ChangeMessage cm : msgs) {
+      if (self.equals(cm.getAuthor())) {
+        return true;
+      } else if (changeOwnerId.equals(cm.getAuthor())) {
+        return false;
+      }
+    }
     return false;
   }
 
@@ -741,101 +792,100 @@
 
     if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
       try {
-        PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
-        out.commit = new CommitInfo();
-        out.commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
-        out.commit.author = toGitPerson(info.getAuthor());
-        out.commit.committer = toGitPerson(info.getCommitter());
-        out.commit.subject = info.getSubject();
-        out.commit.message = info.getMessage();
-
-        for (ParentInfo parent : info.getParents()) {
-          CommitInfo i = new CommitInfo();
-          i.commit = parent.id.get();
-          i.subject = parent.shortMessage;
-          out.commit.parents.add(i);
-        }
+        out.commit = toCommit(in);
       } catch (PatchSetInfoNotAvailableException e) {
         log.warn("Cannot load PatchSetInfo " + in.getId(), e);
       }
     }
 
     if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
-      PatchList list;
       try {
-        list = patchListCache.get(cd.change(db), in);
+        out.files = fileInfoJson.toFileInfoMap(cd.change(db), in);
+        out.files.remove(Patch.COMMIT_MSG);
       } catch (PatchListNotAvailableException e) {
         log.warn("Cannot load PatchList " + in.getId(), e);
-        list = null;
-      }
-      if (list != null) {
-        out.files = Maps.newTreeMap();
-        for (PatchListEntry e : list.getPatches()) {
-          if (Patch.COMMIT_MSG.equals(e.getNewName())) {
-            continue;
-          }
-
-          FileInfo d = new FileInfo();
-          d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
-              ? e.getChangeType().getCode()
-              : null;
-          d.oldPath = e.getOldName();
-          if (e.getPatchType() == Patch.PatchType.BINARY) {
-            d.binary = true;
-          } else {
-            d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
-            d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
-          }
-
-          FileInfo o = out.files.put(e.getNewName(), d);
-          if (o != null) {
-            // This should only happen on a delete-add break created by JGit
-            // 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();
-            if (o.binary != null && o.binary) {
-              d.binary = true;
-            }
-            if (o.linesInserted != null) {
-              d.linesInserted = o.linesInserted;
-            }
-            if (o.linesDeleted != null) {
-              d.linesDeleted = o.linesDeleted;
-            }
-          }
-        }
       }
     }
+
+    if ((out.isCurrent || (out.draft != null && out.draft))
+        && has(CURRENT_ACTIONS)
+        && userProvider.get().isIdentifiedUser()) {
+      out.actions = Maps.newTreeMap();
+      for (UiAction.Description d : UiActions.from(
+          revisions,
+          new RevisionResource(new ChangeResource(control(cd)), in),
+          userProvider)) {
+        out.actions.put(d.getId(), new ActionInfo(d));
+      }
+    }
+
+    if (has(DRAFT_COMMENTS)
+        && userProvider.get().isIdentifiedUser()) {
+      IdentifiedUser user = (IdentifiedUser)userProvider.get();
+      out.hasDraftComments =
+          db.get().patchComments()
+              .draftByPatchSetAuthor(in.getId(), user.getAccountId())
+              .iterator().hasNext()
+          ? true
+          : null;
+    }
+
     return out;
   }
 
+  CommitInfo toCommit(PatchSet in)
+      throws PatchSetInfoNotAvailableException {
+    PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
+    CommitInfo commit = new CommitInfo();
+    commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
+    commit.author = toGitPerson(info.getAuthor());
+    commit.committer = toGitPerson(info.getCommitter());
+    commit.subject = info.getSubject();
+    commit.message = info.getMessage();
+
+    for (ParentInfo parent : info.getParents()) {
+      CommitInfo i = new CommitInfo();
+      i.commit = parent.id.get();
+      i.subject = parent.shortMessage;
+      commit.parents.add(i);
+    }
+    return commit;
+  }
+
   private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
       throws OrmException {
     Map<String, FetchInfo> r = Maps.newLinkedHashMap();
-    String refName = in.getRefName();
-    ChangeControl ctl = control(cd);
-    if (ctl != null && ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
-      if (urls.git != null) {
-        r.put("git", new FetchInfo(urls.git
-            + cd.change(db).getProject().get(), refName));
+
+    for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
+      String schemeName = e.getExportName();
+      DownloadScheme scheme = e.getProvider().get();
+      if (!scheme.isEnabled()
+          || (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
+        continue;
       }
-    }
-    if (urls.http != null) {
-      r.put("http", new FetchInfo(urls.http
-          + cd.change(db).getProject().get(), refName));
-    } else {
-      String http = urlProvider.get();
-      if (!Strings.isNullOrEmpty(http)) {
-        r.put("http", new FetchInfo(http
-            + cd.change(db).getProject().get(), refName));
+
+      ChangeControl ctl = control(cd);
+      if (!scheme.isAuthSupported()
+          && !ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
+        continue;
       }
-    }
-    if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
-      HostKey host = sshInfo.getHostKeys().get(0);
-      r.put("ssh", new FetchInfo(String.format(
-          "ssh://%s/%s",
-          host.getHost(), cd.change(db).getProject().get()),
-          refName));
+
+      String projectName = ctl.getProject().getNameKey().get();
+      String url = scheme.getUrl(projectName);
+      String refName = in.getRefName();
+      FetchInfo fetchInfo = new FetchInfo(url, refName);
+      r.put(schemeName, fetchInfo);
+
+      if (has(DOWNLOAD_COMMANDS)) {
+        for (DynamicMap.Entry<DownloadCommand> e2 : downloadCommands) {
+          String commandName = e2.getExportName();
+          DownloadCommand command = e2.getProvider().get();
+          String c = command.getCommand(scheme, projectName, refName);
+          if (c != null) {
+            fetchInfo.addCommand(commandName, c);
+          }
+        }
+      }
     }
 
     return r;
@@ -865,11 +915,12 @@
     Boolean reviewed;
     Boolean mergeable;
 
-    String _sortkey;
-    int _number;
+    public String _sortkey;
+    public int _number;
 
     AccountInfo owner;
 
+    Map<String, ActionInfo> actions;
     Map<String, LabelInfo> labels;
     Map<String, Collection<String>> permitted_labels;
     Collection<AccountInfo> removable_reviewers;
@@ -890,20 +941,30 @@
   static class RevisionInfo {
     private transient boolean isCurrent;
     Boolean draft;
+    Boolean hasDraftComments;
     int _number;
     Map<String, FetchInfo> fetch;
     CommitInfo commit;
-    Map<String, FileInfo> files;
+    Map<String, FileInfoJson.FileInfo> files;
+    Map<String, ActionInfo> actions;
   }
 
   static class FetchInfo {
     String url;
     String ref;
+    Map<String, String> commands;
 
     FetchInfo(String url, String ref) {
       this.url = url;
       this.ref = ref;
     }
+
+    void addCommand(String name, String command) {
+      if (commands == null) {
+        commands = Maps.newTreeMap();
+      }
+      commands.put(name, command);
+    }
   }
 
   static class GitPerson {
@@ -914,6 +975,7 @@
   }
 
   static class CommitInfo {
+    final String kind = "gerritcodereview#commit";
     String commit;
     List<CommitInfo> parents;
     GitPerson author;
@@ -922,14 +984,6 @@
     String message;
   }
 
-  static class FileInfo {
-    Character status;
-    Boolean binary;
-    String oldPath;
-    Integer linesInserted;
-    Integer linesDeleted;
-  }
-
   static class LabelInfo {
     transient SubmitRecord.Label.Status _status;
 
@@ -954,6 +1008,7 @@
 
   static class ApprovalInfo extends AccountInfo {
     Integer value;
+    Timestamp date;
 
     ApprovalInfo(Account.Id id) {
       super(id);
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 be3081a..b0562a9 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
@@ -14,13 +14,22 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Objects;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
 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.Change;
+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.ProjectState;
 import com.google.inject.TypeLiteral;
 
-public class ChangeResource implements RestResource {
+import org.eclipse.jgit.lib.ObjectId;
+
+public class ChangeResource implements RestResource, HasETag {
   public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND =
       new TypeLiteral<RestView<ChangeResource>>() {};
 
@@ -41,4 +50,24 @@
   public Change getChange() {
     return getControl().getChange();
   }
+
+  @Override
+  public String getETag() {
+    CurrentUser user = control.getCurrentUser();
+    Hasher h = Hashing.md5().newHasher()
+      .putLong(getChange().getLastUpdatedOn().getTime())
+      .putInt(getChange().getRowVersion())
+      .putBoolean(user.getStarredChanges().contains(getChange().getId()))
+      .putInt(user.isIdentifiedUser()
+          ? ((IdentifiedUser) user).getAccountId().get()
+          : 0);
+
+    byte[] buf = new byte[20];
+    for (ProjectState p : control.getProjectControl().getProjectState().tree()) {
+      ObjectId id = p.getConfig().getRevision();
+      Objects.firstNonNull(id, ObjectId.zeroId()).copyRawTo(buf, 0);
+      h.putBytes(buf);
+    }
+    return h.hash().toString();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index 45328b1..e93a0d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 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.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.QueryChanges;
@@ -37,24 +38,27 @@
 public class ChangesCollection implements
     RestCollection<TopLevelResource, ChangeResource> {
   private final Provider<ReviewDb> db;
-  private final ChangeControl.Factory changeControlFactory;
+  private final Provider<CurrentUser> user;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final Provider<QueryChanges> queryFactory;
   private final DynamicMap<RestView<ChangeResource>> views;
 
   @Inject
   ChangesCollection(
       Provider<ReviewDb> dbProvider,
-      ChangeControl.Factory changeControlFactory,
+      Provider<CurrentUser> user,
+      ChangeControl.GenericFactory changeControlFactory,
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views) {
     this.db = dbProvider;
+    this.user = user;
     this.changeControlFactory = changeControlFactory;
     this.queryFactory = queryFactory;
     this.views = views;
   }
 
   @Override
-  public RestView<TopLevelResource> list() {
+  public QueryChanges list() {
     return queryFactory.get();
   }
 
@@ -74,7 +78,7 @@
 
     ChangeControl control;
     try {
-      control = changeControlFactory.validateFor(changes.get(0));
+      control = changeControlFactory.validateFor(changes.get(0), user.get());
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(id);
     }
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
new file mode 100644
index 0000000..18f45bb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -0,0 +1,102 @@
+// 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.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.CherryPick.Input;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.RefControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class CherryPick implements RestModifyView<RevisionResource, Input>,
+    UiAction<RevisionResource> {
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<CherryPickChange> cherryPickChange;
+  private final ChangeJson json;
+
+  static class Input {
+    String message;
+    String destination;
+  }
+
+  @Inject
+  CherryPick(Provider<ReviewDb> dbProvider,
+      Provider<CherryPickChange> cherryPickChange,
+      ChangeJson json) {
+    this.dbProvider = dbProvider;
+    this.cherryPickChange = cherryPickChange;
+    this.json = json;
+  }
+
+  @Override
+  public Object apply(RevisionResource revision, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      Exception {
+    final ChangeControl control = revision.getControl();
+
+    if (input.message == null || input.message.trim().isEmpty()) {
+      throw new BadRequestException("message must be non-empty");
+    } else if (input.destination == null || input.destination.trim().isEmpty()) {
+      throw new BadRequestException("destination must be non-empty");
+    }
+
+    ReviewDb db = dbProvider.get();
+    if (!control.isVisible(db)) {
+      throw new AuthException("Cherry pick not permitted");
+    }
+
+    String refName = input.destination;
+    if (!refName.startsWith("refs/")) {
+      refName = "refs/heads/" + input.destination;
+    }
+
+    RefControl refControl = control.getProjectControl().controlForRef(refName);
+    if (!refControl.canUpload()) {
+      throw new AuthException("Not allowed to cherry pick "
+          + revision.getChange().getId().toString() + " to "
+          + input.destination);
+    }
+
+    final PatchSet.Id patchSetId = revision.getPatchSet().getId();
+    try {
+      Change.Id cherryPickedChangeId = cherryPickChange.get().cherryPick(
+          patchSetId, input.message,
+          input.destination, refControl);
+      return json.format(cherryPickedChangeId);
+    } catch (InvalidChangeOperationException e) {
+      throw new BadRequestException(e.getMessage());
+    } catch (MergeException  e) {
+      throw new ResourceConflictException(e.getMessage());
+    }
+  }
+
+  @Override
+  public UiAction.Description getDescription(RevisionResource resource) {
+    return new UiAction.Description()
+      .setLabel("Cherry Pick")
+      .setTitle("Cherry pick change to a different branch")
+      .setVisible(resource.getControl().getProjectControl().canUpload());
+  }
+}
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
new file mode 100644
index 0000000..f206a3d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -0,0 +1,264 @@
+// 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.errors.EmailException;
+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.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.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.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;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
+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;
+import java.util.List;
+
+public class CherryPickChange {
+
+  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+
+  private final ReviewDb db;
+  private final GitRepositoryManager gitManager;
+  private final PersonIdent myIdent;
+  private final IdentifiedUser currentUser;
+  private final CommitValidators.Factory commitValidatorsFactory;
+  private final ChangeInserter.Factory changeInserterFactory;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  final MergeUtil.Factory mergeUtilFactory;
+
+  @Inject
+  CherryPickChange(final ReviewDb db, @GerritPersonIdent final PersonIdent myIdent,
+      final GitRepositoryManager gitManager, final IdentifiedUser currentUser,
+      final CommitValidators.Factory commitValidatorsFactory,
+      final ChangeInserter.Factory changeInserterFactory,
+      final PatchSetInserter.Factory patchSetInserterFactory,
+      final MergeUtil.Factory mergeUtilFactory) {
+    this.db = db;
+    this.gitManager = gitManager;
+    this.myIdent = myIdent;
+    this.currentUser = currentUser;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.changeInserterFactory = changeInserterFactory;
+    this.patchSetInserterFactory = patchSetInserterFactory;
+    this.mergeUtilFactory = mergeUtilFactory;
+  }
+
+  public Change.Id cherryPick(final PatchSet.Id patchSetId,
+      final String message, final String destinationBranch,
+      final RefControl refControl) throws NoSuchChangeException,
+      EmailException, OrmException, MissingObjectException,
+      IncorrectObjectTypeException, IOException,
+      InvalidChangeOperationException, MergeException {
+
+    final Change.Id changeId = patchSetId.getParentKey();
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    if (patch == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+    if (destinationBranch == null || destinationBranch.length() == 0) {
+      throw new InvalidChangeOperationException(
+          "Cherry Pick: Destination branch cannot be null or empty");
+    }
+
+    Project.NameKey project = db.changes().get(changeId).getProject();
+    final Repository git;
+    try {
+      git = gitManager.openRepository(project);
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchChangeException(changeId, e);
+    }
+
+    try {
+      RevWalk revWalk = new RevWalk(git);
+      try {
+        Ref destRef = git.getRef(destinationBranch);
+        if (destRef == null) {
+          throw new InvalidChangeOperationException("Branch "
+              + destinationBranch + " does not exist.");
+        }
+
+        final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
+
+        RevCommit commitToCherryPick =
+            revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+
+        PersonIdent committerIdent =
+            currentUser.newCommitterIdent(myIdent.getWhen(),
+                myIdent.getTimeZone());
+
+        final ObjectId computedChangeId =
+            ChangeIdUtil
+                .computeChangeId(commitToCherryPick.getTree(), mergeTip,
+                    commitToCherryPick.getAuthorIdent(), myIdent, message);
+        String commitMessage = ChangeIdUtil.insertId(message, computedChangeId);
+
+        RevCommit cherryPickCommit;
+        ObjectInserter oi = git.newObjectInserter();
+        try {
+          ProjectState projectState = refControl.getProjectControl().getProjectState();
+          cherryPickCommit =
+              mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
+                  commitToCherryPick, committerIdent, commitMessage, revWalk);
+        } finally {
+          oi.release();
+        }
+
+        if (cherryPickCommit == null) {
+          throw new MergeException(
+              "Could not create a merge commit during the cherry pick");
+        }
+
+        Change.Key changeKey;
+        final List<String> idList = cherryPickCommit.getFooterLines(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());
+        }
+
+        List<Change> destChanges =
+            db.changes()
+                .byBranchKey(
+                    new Branch.NameKey(db.changes().get(changeId).getProject(),
+                        destRef.getName()), changeKey).toList();
+
+        if (destChanges.size() > 1) {
+          throw new InvalidChangeOperationException("Several changes with key "
+              + changeKey + " resides 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), patchSetId,
+              cherryPickCommit, refControl);
+        } else {
+          // Change key not found on destination branch. We can create a new
+          // change.
+          return createNewChange(git, revWalk, changeKey, project, patchSetId, destRef,
+              cherryPickCommit, refControl);
+        }
+      } finally {
+        revWalk.release();
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  private Change.Id insertPatchSet(Repository git, RevWalk revWalk, Change change,
+      PatchSet.Id patchSetId, RevCommit cherryPickCommit,
+      RefControl refControl) throws InvalidChangeOperationException,
+      IOException, OrmException, NoSuchChangeException {
+    final PatchSetInserter inserter = patchSetInserterFactory
+        .create(git, revWalk, refControl, currentUser, change, cherryPickCommit);
+    final PatchSet.Id newPatchSetId = inserter.getPatchSetId();
+    final PatchSet current = db.patchSets().get(change.currentPatchSetId());
+    inserter
+      .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
+      .setDraft(current.isDraft())
+      .insert();
+    return change.getId();
+  }
+
+  private Change.Id createNewChange(Repository git, RevWalk revWalk,
+      Change.Key changeKey, Project.NameKey project, PatchSet.Id patchSetId,
+      Ref destRef, RevCommit cherryPickCommit, RefControl refControl)
+      throws OrmException, InvalidChangeOperationException, IOException {
+    Change change =
+        new Change(changeKey, new Change.Id(db.nextChangeId()),
+            currentUser.getAccountId(), new Branch.NameKey(project,
+                destRef.getName()), TimeUtil.nowTs());
+    ChangeInserter ins =
+        changeInserterFactory.create(refControl, change, cherryPickCommit);
+    PatchSet newPatchSet = ins.getPatchSet();
+
+    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, currentUser);
+
+    try {
+      commitValidators.validateForGerritCommits(commitReceivedEvent);
+    } catch (CommitValidationException e) {
+      throw new InvalidChangeOperationException(e.getMessage());
+    }
+
+    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.setMessage(buildChangeMessage(patchSetId, change)).insert();
+
+    return change.getId();
+  }
+
+  private ChangeMessage buildChangeMessage(PatchSet.Id patchSetId, Change dest)
+      throws OrmException {
+    ChangeMessage cmsg = new ChangeMessage(
+        new ChangeMessage.Key(
+            patchSetId.getParentKey(), ChangeUtil.messageUUID(db)),
+        currentUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
+    StringBuilder msgBuf =
+        new StringBuilder("Patch Set " + patchSetId.get()
+            + ": Cherry Picked");
+    msgBuf.append("\n\n");
+    msgBuf.append("This patchset was cherry picked to change: "
+        + dest.getKey().get());
+    cmsg.setMessage(msgBuf.toString());
+    return cmsg;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
index 798b9c6..f4c148a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
@@ -10,21 +10,20 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.CommentRange;
 import com.google.gerrit.server.account.AccountInfo;
 
 import java.sql.Timestamp;
 
 public class CommentInfo {
-  static enum Side {
-    PARENT, REVISION;
-  }
 
   final String kind = "gerritcodereview#comment";
   String id;
@@ -35,6 +34,7 @@
   String message;
   Timestamp updated;
   AccountInfo author;
+  CommentRange range;
 
   CommentInfo(PatchLineComment c, AccountInfo.Loader accountLoader) {
     id = Url.encode(c.getKey().get());
@@ -48,6 +48,7 @@
     inReplyTo = Url.encode(c.getParentUuid());
     message = Strings.emptyToNull(c.getMessage());
     updated = c.getWrittenOn();
+    range = c.getRange();
     if (accountLoader != null) {
       author = accountLoader.get(c.getAuthor());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
index 5157af4..229e072 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -23,10 +24,10 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.PutDraft.Input;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -50,18 +51,22 @@
       throw new BadRequestException("message must be non-empty");
     } else if (in.line != null && in.line <= 0) {
       throw new BadRequestException("line must be > 0");
+    } else if (in.line != null && in.range != null && in.line != in.range.getEndLine()) {
+      throw new BadRequestException("range endLine must be on the same line as the comment");
     }
 
+    int line = in.line != null
+        ? in.line
+        : in.range != null ? in.range.getEndLine() : 0;
+
     PatchLineComment c = new PatchLineComment(
         new PatchLineComment.Key(
             new Patch.Key(rsrc.getPatchSet().getId(), in.path),
             ChangeUtil.messageUUID(db.get())),
-        in.line != null ? in.line : 0,
-        rsrc.getAccountId(),
-        Url.decode(in.inReplyTo));
-    c.setStatus(Status.DRAFT);
-    c.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+        line, rsrc.getAccountId(), Url.decode(in.inReplyTo), TimeUtil.nowTs());
+    c.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
     c.setMessage(in.message.trim());
+    c.setRange(in.range);
     db.get().patchComments().insert(Collections.singleton(c));
     return Response.created(new CommentInfo(c, null));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
new file mode 100644
index 0000000..767d5ee
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
@@ -0,0 +1,94 @@
+// 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.change;
+
+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.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.change.DeleteDraftChange.Input;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class DeleteDraftChange implements
+    RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
+  public static class Input {
+  }
+
+  protected final Provider<ReviewDb> dbProvider;
+  private final GitRepositoryManager gitManager;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final ChangeIndexer indexer;
+
+  @Inject
+  public DeleteDraftChange(Provider<ReviewDb> dbProvider,
+      GitRepositoryManager gitManager,
+      GitReferenceUpdated gitRefUpdated,
+      PatchSetInfoFactory patchSetInfoFactory,
+      ChangeIndexer indexer) {
+    this.dbProvider = dbProvider;
+    this.gitManager = gitManager;
+    this.gitRefUpdated = gitRefUpdated;
+    this.indexer = indexer;
+  }
+
+  @Override
+  public Object apply(ChangeResource rsrc, Input input)
+      throws ResourceConflictException, AuthException,
+      ResourceNotFoundException, OrmException, IOException {
+    if (rsrc.getChange().getStatus() != Status.DRAFT) {
+      throw new ResourceConflictException("Change is not a draft");
+    }
+
+    if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
+      throw new AuthException("Not permitted to delete this draft change");
+    }
+
+    try {
+      ChangeUtil.deleteDraftChange(rsrc.getChange().getId(),
+          gitManager, gitRefUpdated, dbProvider.get(), indexer);
+    } catch (NoSuchChangeException e) {
+      throw new ResourceNotFoundException(e.getMessage());
+    }
+
+    return Response.none();
+  }
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource rsrc) {
+    try {
+      return new UiAction.Description()
+        .setTitle(String.format("Delete draft change %d",
+            rsrc.getChange().getChangeId()))
+        .setVisible(rsrc.getChange().getStatus() == Status.DRAFT
+            && rsrc.getControl().canDeleteDraft(dbProvider.get()));
+    } catch (OrmException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
new file mode 100644
index 0000000..3265c81
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -0,0 +1,163 @@
+// 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.change;
+
+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.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+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.ChangeUtil;
+import com.google.gerrit.server.change.DeleteDraftPatchSet.Input;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Input>,
+    UiAction<RevisionResource> {
+  public static class Input {
+  }
+
+  protected final Provider<ReviewDb> dbProvider;
+  private final GitRepositoryManager gitManager;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ChangeIndexer indexer;
+
+  @Inject
+  public DeleteDraftPatchSet(Provider<ReviewDb> dbProvider,
+      GitRepositoryManager gitManager,
+      GitReferenceUpdated gitRefUpdated,
+      PatchSetInfoFactory patchSetInfoFactory,
+      ChangeIndexer indexer) {
+    this.dbProvider = dbProvider;
+    this.gitManager = gitManager;
+    this.gitRefUpdated = gitRefUpdated;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.indexer = indexer;
+  }
+
+  @Override
+  public Object apply(RevisionResource rsrc, Input input)
+      throws ResourceNotFoundException, AuthException, OrmException,
+      IOException, ResourceConflictException {
+    PatchSet patchSet = rsrc.getPatchSet();
+    PatchSet.Id patchSetId = patchSet.getId();
+    Change change = rsrc.getChange();
+
+    if (!patchSet.isDraft()) {
+      throw new ResourceConflictException("Patch set is not a draft.");
+    }
+
+    if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
+      throw new AuthException("Not permitted to delete this draft patch set");
+    }
+
+    deleteDraftPatchSet(patchSet, change);
+    deleteOrUpdateDraftChange(patchSetId, change);
+
+    return Response.none();
+  }
+
+  @Override
+  public UiAction.Description getDescription(RevisionResource rsrc) {
+    try {
+      int psCount = dbProvider.get().patchSets()
+          .byChange(rsrc.getChange().getId()).toList().size();
+      return new UiAction.Description()
+        .setTitle(String.format("Delete draft revision %d",
+            rsrc.getPatchSet().getPatchSetId()))
+        .setVisible(rsrc.getPatchSet().isDraft()
+            && rsrc.getControl().canDeleteDraft(dbProvider.get())
+            && psCount > 1);
+    } catch (OrmException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private void deleteDraftPatchSet(PatchSet patchSet, Change change)
+      throws ResourceNotFoundException, OrmException, IOException {
+    try {
+      ChangeUtil.deleteOnlyDraftPatchSet(patchSet,
+          change, gitManager, gitRefUpdated, dbProvider.get());
+    } catch (NoSuchChangeException e) {
+      throw new ResourceNotFoundException(e.getMessage());
+    }
+  }
+
+  private void deleteOrUpdateDraftChange(PatchSet.Id patchSetId,
+      Change change) throws OrmException, ResourceNotFoundException,
+      IOException {
+    if (dbProvider.get()
+            .patchSets()
+            .byChange(change.getId())
+            .toList().size() == 0) {
+      deleteDraftChange(patchSetId);
+    } else {
+      if (change.currentPatchSetId().equals(patchSetId)) {
+        updateCurrentPatchSet(dbProvider.get(), change,
+            previousPatchSetInfo(patchSetId));
+      }
+    }
+  }
+
+  private void deleteDraftChange(PatchSet.Id patchSetId)
+      throws OrmException, IOException, ResourceNotFoundException {
+    try {
+      ChangeUtil.deleteDraftChange(patchSetId,
+          gitManager, gitRefUpdated, dbProvider.get(), indexer);
+    } catch (NoSuchChangeException e) {
+      throw new ResourceNotFoundException(e.getMessage());
+    }
+  }
+
+  private PatchSetInfo previousPatchSetInfo(PatchSet.Id patchSetId)
+      throws ResourceNotFoundException {
+    try {
+      return patchSetInfoFactory.get(dbProvider.get(),
+          new PatchSet.Id(patchSetId.getParentKey(),
+              patchSetId.get() - 1));
+    } catch (PatchSetInfoNotAvailableException e) {
+        throw new ResourceNotFoundException(e.getMessage());
+    }
+  }
+
+  private static void updateCurrentPatchSet(final ReviewDb db,
+      final Change change, final PatchSetInfo psInfo)
+      throws OrmException {
+    db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+      @Override
+      public Change update(Change c) {
+        c.setCurrentPatchSet(psInfo);
+        ChangeUtil.updated(c);
+        return c;
+      }
+    });
+  }
+}
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 5bfffac..c58fc6c 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
@@ -25,12 +25,15 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.DeleteReviewer.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import java.io.IOException;
 import java.util.List;
 
 public class DeleteReviewer implements RestModifyView<ReviewerResource, Input> {
@@ -38,15 +41,18 @@
   }
 
   private final Provider<ReviewDb> dbProvider;
+  private final ChangeIndexer indexer;
 
   @Inject
-  DeleteReviewer(Provider<ReviewDb> dbProvider) {
+  DeleteReviewer(Provider<ReviewDb> dbProvider, ChangeIndexer indexer) {
     this.dbProvider = dbProvider;
+    this.indexer = indexer;
   }
 
   @Override
   public Object apply(ReviewerResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException, OrmException {
+      throws AuthException, ResourceNotFoundException, OrmException,
+      IOException {
     ChangeControl control = rsrc.getControl();
     Change.Id changeId = rsrc.getChange().getId();
     ReviewDb db = dbProvider.get();
@@ -63,11 +69,13 @@
       if (del.isEmpty()) {
         throw new ResourceNotFoundException();
       }
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
       db.patchSetApprovals().delete(del);
       db.commit();
     } finally {
       db.rollback();
     }
+    indexer.index(rsrc.getChange());
     return Response.none();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
index 8282d7c..8fc05be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -73,7 +72,7 @@
   }
 
   private void checkIdentifiedUser() throws AuthException {
-    if (!(user.get() instanceof IdentifiedUser)) {
+    if (!(user.get().isIdentifiedUser())) {
       throw new AuthException("drafts only available to authenticated users");
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
new file mode 100644
index 0000000..a634b7c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
@@ -0,0 +1,126 @@
+// 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.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+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.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.EditMessage.Input;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+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 org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+class EditMessage implements RestModifyView<RevisionResource, Input>,
+    UiAction<RevisionResource> {
+
+  private final Provider<ReviewDb> dbProvider;
+  private final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory;
+  private final GitRepositoryManager gitManager;
+  private final PersonIdent myIdent;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final ChangeJson json;
+
+  static class Input {
+    @DefaultInput
+    String message;
+  }
+
+  @Inject
+  EditMessage(final Provider<ReviewDb> dbProvider,
+      final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
+      final GitRepositoryManager gitManager,
+      final PatchSetInserter.Factory patchSetInserterFactory,
+      @GerritPersonIdent final PersonIdent myIdent,
+      ChangeJson json) {
+    this.dbProvider = dbProvider;
+    this.commitMessageEditedSenderFactory = commitMessageEditedSenderFactory;
+    this.gitManager = gitManager;
+    this.myIdent = myIdent;
+    this.patchSetInserterFactory = patchSetInserterFactory;
+    this.json = json;
+  }
+
+  @Override
+  public ChangeInfo apply(RevisionResource rsrc, Input input)
+      throws BadRequestException, ResourceConflictException, EmailException,
+      OrmException, ResourceNotFoundException, IOException {
+    if (Strings.isNullOrEmpty(input.message)) {
+      throw new BadRequestException("message must be non-empty");
+    }
+
+    final Repository git;
+    try {
+      git = gitManager.openRepository(rsrc.getChange().getProject());
+    } catch (RepositoryNotFoundException e) {
+      throw new ResourceNotFoundException(e.getMessage());
+    }
+
+    try {
+      return json.format(ChangeUtil.editCommitMessage(
+          rsrc.getPatchSet().getId(),
+          rsrc.getControl().getRefControl(),
+          (IdentifiedUser) rsrc.getControl().getCurrentUser(),
+          input.message, dbProvider.get(),
+          commitMessageEditedSenderFactory, git, myIdent,
+          patchSetInserterFactory));
+    } catch (InvalidChangeOperationException e) {
+      throw new BadRequestException(e.getMessage());
+    } catch (MissingObjectException e) {
+      throw new ResourceConflictException(e.getMessage());
+    } catch (IncorrectObjectTypeException e) {
+      throw new ResourceConflictException(e.getMessage());
+    } catch (PatchSetInfoNotAvailableException e) {
+      throw new ResourceConflictException(e.getMessage());
+    } catch (NoSuchChangeException e) {
+      throw new ResourceNotFoundException();
+    } finally {
+      git.close();
+    }
+  }
+
+  @Override
+  public UiAction.Description getDescription(RevisionResource resource) {
+    PatchSet.Id current = resource.getChange().currentPatchSetId();
+    return new UiAction.Description()
+        .setLabel("Edit commit message")
+        .setVisible(resource.getChange().getStatus().isOpen()
+            && resource.getPatchSet().getId().equals(current)
+            && resource.getControl().canAddPatchSet());
+  }
+}
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
new file mode 100644
index 0000000..65b96b3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -0,0 +1,96 @@
+// 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.change;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
+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.AccountDiffPreference.Whitespace;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.Map;
+
+public class FileInfoJson {
+  private final PatchListCache patchListCache;
+
+  @Inject
+  FileInfoJson(PatchListCache patchListCache) {
+    this.patchListCache = patchListCache;
+  }
+
+  Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet)
+      throws PatchListNotAvailableException {
+    return toFileInfoMap(change, patchSet, null);
+  }
+
+  Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet, @Nullable PatchSet base)
+      throws PatchListNotAvailableException {
+    ObjectId a = (base == null)
+        ? null
+        : ObjectId.fromString(base.getRevision().get());
+    ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
+    PatchList list = patchListCache.get(
+        new PatchListKey(change.getProject(), a, b, Whitespace.IGNORE_NONE));
+
+    Map<String, FileInfo> files = Maps.newTreeMap();
+    for (PatchListEntry e : list.getPatches()) {
+      FileInfoJson.FileInfo d = new FileInfoJson.FileInfo();
+      d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
+          ? e.getChangeType().getCode() : null;
+      d.oldPath = e.getOldName();
+      if (e.getPatchType() == Patch.PatchType.BINARY) {
+        d.binary = true;
+      } else {
+        d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
+        d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
+      }
+
+      FileInfoJson.FileInfo o = files.put(e.getNewName(), d);
+      if (o != null) {
+        // This should only happen on a delete-add break created by JGit
+        // 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();
+        if (o.binary != null && o.binary) {
+          d.binary = true;
+        }
+        if (o.linesInserted != null) {
+          d.linesInserted = o.linesInserted;
+        }
+        if (o.linesDeleted != null) {
+          d.linesDeleted = o.linesDeleted;
+        }
+      }
+    }
+    return files;
+  }
+
+  static class FileInfo {
+    Character status;
+    Boolean binary;
+    String oldPath;
+    Integer linesInserted;
+    Integer linesDeleted;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
new file mode 100644
index 0000000..521e8c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
@@ -0,0 +1,50 @@
+// 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.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.inject.TypeLiteral;
+
+public class FileResource implements RestResource {
+  public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
+      new TypeLiteral<RestView<FileResource>>() {};
+
+  private final RevisionResource rev;
+  private final Patch.Key key;
+
+  FileResource(RevisionResource rev, String name) {
+    this.rev = rev;
+    this.key = new Patch.Key(rev.getPatchSet().getId(), name);
+  }
+
+  public Patch.Key getPatchKey() {
+    return key;
+  }
+
+  public boolean isCacheable() {
+    return rev.isCacheable();
+  }
+
+  Account.Id getAccountId() {
+    return rev.getAccountId();
+  }
+
+  RevisionResource getRevision() {
+    return rev;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
new file mode 100644
index 0000000..98e2ee9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -0,0 +1,276 @@
+// 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.change;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+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.CacheControl;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountPatchReview;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.FileInfoJson.FileInfo;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.concurrent.TimeUnit;
+
+class Files implements ChildCollection<RevisionResource, FileResource> {
+  private final DynamicMap<RestView<FileResource>> views;
+  private final Provider<ListFiles> list;
+
+  @Inject
+  Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) {
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public DynamicMap<RestView<FileResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<RevisionResource> list() throws AuthException {
+    return list.get();
+  }
+
+  @Override
+  public FileResource parse(RevisionResource rev, IdString id)
+      throws ResourceNotFoundException, OrmException, AuthException {
+    return new FileResource(rev, id.get());
+  }
+
+  private static final class ListFiles implements RestReadView<RevisionResource> {
+    private static final Logger log = LoggerFactory.getLogger(ListFiles.class);
+
+    @Option(name = "--base", metaVar = "revision-id")
+    String base;
+
+    @Option(name = "--reviewed")
+    boolean reviewed;
+
+    private final Provider<ReviewDb> db;
+    private final Provider<CurrentUser> self;
+    private final FileInfoJson fileInfoJson;
+    private final Provider<Revisions> revisions;
+    private final GitRepositoryManager gitManager;
+    private final PatchListCache patchListCache;
+
+    @Inject
+    ListFiles(Provider<ReviewDb> db,
+        Provider<CurrentUser> self,
+        FileInfoJson fileInfoJson,
+        Provider<Revisions> revisions,
+        GitRepositoryManager gitManager,
+        PatchListCache patchListCache) {
+      this.db = db;
+      this.self = self;
+      this.fileInfoJson = fileInfoJson;
+      this.revisions = revisions;
+      this.gitManager = gitManager;
+      this.patchListCache = patchListCache;
+    }
+
+    @Override
+    public Object apply(RevisionResource resource)
+        throws ResourceNotFoundException, OrmException,
+        PatchListNotAvailableException, BadRequestException, AuthException {
+      if (base != null && reviewed) {
+        throw new BadRequestException("cannot combine base and reviewed");
+      } else if (reviewed) {
+        return reviewed(resource);
+      }
+
+      PatchSet basePatchSet = null;
+      if (base != null) {
+        RevisionResource baseResource = revisions.get().parse(
+            resource.getChangeResource(), IdString.fromDecoded(base));
+        basePatchSet = baseResource.getPatchSet();
+      }
+      Response<Map<String, FileInfo>> r = Response.ok(fileInfoJson.toFileInfoMap(
+          resource.getChange(),
+          resource.getPatchSet(),
+          basePatchSet));
+      if (resource.isCacheable()) {
+        r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+      }
+      return r;
+    }
+
+    private Object reviewed(RevisionResource resource)
+        throws AuthException, OrmException {
+      CurrentUser user = self.get();
+      if (!(user.isIdentifiedUser())) {
+        throw new AuthException("Authentication required");
+      }
+
+      Account.Id userId = ((IdentifiedUser) user).getAccountId();
+      List<String> r = scan(userId, resource.getPatchSet().getId());
+
+      if (r.isEmpty() && 1 < resource.getPatchSet().getPatchSetId()) {
+        for (Integer id : reverseSortPatchSets(resource)) {
+          PatchSet.Id old = new PatchSet.Id(resource.getChange().getId(), id);
+          List<String> o = scan(userId, old);
+          if (!o.isEmpty()) {
+            try {
+              r = copy(Sets.newHashSet(o), old, resource, userId);
+            } catch (IOException e) {
+              log.warn("Cannot copy patch review flags", e);
+            } catch (PatchListNotAvailableException e) {
+              log.warn("Cannot copy patch review flags", e);
+            }
+            break;
+          }
+        }
+      }
+
+      return r;
+    }
+
+    private List<String> scan(Account.Id userId, PatchSet.Id psId)
+        throws OrmException {
+      List<String> r = Lists.newArrayList();
+      for (AccountPatchReview w : db.get().accountPatchReviews()
+          .byReviewer(userId, psId)) {
+        r.add(w.getKey().getPatchKey().getFileName());
+      }
+      return r;
+    }
+
+    private List<Integer> reverseSortPatchSets(
+        RevisionResource resource) throws OrmException {
+      SortedSet<Integer> ids = Sets.newTreeSet();
+      for (PatchSet p : db.get().patchSets()
+          .byChange(resource.getChange().getId())) {
+        if (p.getPatchSetId() < resource.getPatchSet().getPatchSetId()) {
+          ids.add(p.getPatchSetId());
+        }
+      }
+
+      List<Integer> r = Lists.newArrayList(ids);
+      Collections.reverse(r);
+      return r;
+    }
+
+    private List<String> copy(Set<String> paths, PatchSet.Id old,
+        RevisionResource resource, Account.Id userId) throws IOException,
+        PatchListNotAvailableException, OrmException {
+      Repository git =
+          gitManager.openRepository(resource.getChange().getProject());
+      try {
+        ObjectReader reader = git.newObjectReader();
+        try {
+          PatchList oldList = patchListCache.get(
+              resource.getChange(),
+              db.get().patchSets().get(old));
+
+          PatchList curList = patchListCache.get(
+              resource.getChange(),
+              resource.getPatchSet());
+
+          int sz = paths.size();
+          List<AccountPatchReview> inserts = Lists.newArrayListWithCapacity(sz);
+          List<String> pathList = Lists.newArrayListWithCapacity(sz);
+
+          RevWalk rw = new RevWalk(reader);
+          TreeWalk tw = new TreeWalk(reader);
+          tw.setFilter(PathFilterGroup.createFromStrings(paths));
+          tw.setRecursive(true);
+          int o = tw.addTree(rw.parseCommit(oldList.getNewId()).getTree());
+          int c = tw.addTree(rw.parseCommit(curList.getNewId()).getTree());
+
+          int op = -1;
+          if (oldList.getOldId() != null) {
+            op = tw.addTree(rw.parseCommit(oldList.getOldId()).getTree());
+          }
+
+          int cp = -1;
+          if (curList.getOldId() != null) {
+            cp = tw.addTree(rw.parseCommit(curList.getOldId()).getTree());
+          }
+
+          while (tw.next()) {
+            String path = tw.getPathString();
+            if (tw.getRawMode(o) != 0 && tw.getRawMode(c) != 0
+                && tw.idEqual(o, c)
+                && paths.contains(path)) {
+              // File exists in previously reviewed oldList and in curList.
+              // File content is identical.
+              inserts.add(new AccountPatchReview(
+                  new Patch.Key(
+                      resource.getPatchSet().getId(),
+                      path),
+                    userId));
+              pathList.add(path);
+            } else if (op >= 0 && cp >= 0
+                && tw.getRawMode(o) == 0 && tw.getRawMode(c) == 0
+                && tw.getRawMode(op) != 0 && tw.getRawMode(cp) != 0
+                && tw.idEqual(op, cp)
+                && paths.contains(path)) {
+              // File was deleted in previously reviewed oldList and curList.
+              // File exists in ancestor of oldList and curList.
+              // File content is identical in ancestors.
+              inserts.add(new AccountPatchReview(
+                  new Patch.Key(
+                      resource.getPatchSet().getId(),
+                      path),
+                    userId));
+              pathList.add(path);
+            }
+          }
+          db.get().accountPatchReviews().insert(inserts);
+          return pathList;
+        } finally {
+          reader.release();
+        }
+      } finally {
+        git.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
index 4db5459..7213a94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
@@ -14,13 +14,30 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.kohsuke.args4j.Option;
+
+import java.util.concurrent.TimeUnit;
+
 public class GetChange implements RestReadView<ChangeResource> {
   private final ChangeJson json;
 
+  @Option(name = "-o", multiValued = true, usage = "Output options")
+  void addOption(ListChangesOption o) {
+    json.addOption(o);
+  }
+
+  @Option(name = "-O", usage = "Output option flags, in hex")
+  void setOptionFlagsHex(String hex) {
+    json.addOptions(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+  }
+
   @Inject
   GetChange(ChangeJson json) {
     this.json = json;
@@ -28,6 +45,15 @@
 
   @Override
   public Object apply(ChangeResource rsrc) throws OrmException {
-    return json.format(rsrc);
+    return cache(json.format(rsrc));
+  }
+
+  Object apply(RevisionResource rsrc) throws OrmException {
+    return cache(json.format(rsrc));
+  }
+
+  private Object cache(Object res) {
+    return Response.ok(res)
+        .caching(CacheControl.PRIVATE(0, TimeUnit.SECONDS).setMustRevalidate());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
new file mode 100644
index 0000000..7d29449
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -0,0 +1,44 @@
+// 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.change;
+
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.ChangeJson.CommitInfo;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.concurrent.TimeUnit;
+
+public class GetCommit implements RestReadView<RevisionResource> {
+  private final ChangeJson json;
+
+  @Inject
+  GetCommit(ChangeJson json) {
+    this.json = json;
+  }
+
+  @Override
+  public Response<CommitInfo> apply(RevisionResource resource)
+      throws OrmException, PatchSetInfoNotAvailableException {
+    Response<CommitInfo> r = Response.ok(json.toCommit(resource.getPatchSet()));
+    if (resource.isCacheable()) {
+      r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+    }
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
new file mode 100644
index 0000000..f0d2297
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -0,0 +1,79 @@
+// 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.change;
+
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+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 java.io.IOException;
+import java.io.OutputStream;
+
+public class GetContent implements RestReadView<FileResource> {
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  GetContent(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public BinaryResult apply(FileResource rsrc)
+      throws ResourceNotFoundException, IOException {
+    Project.NameKey project =
+        rsrc.getRevision().getControl().getProject().getNameKey();
+    Repository repo = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        RevCommit commit =
+            rw.parseCommit(ObjectId.fromString(rsrc.getRevision().getPatchSet()
+                .getRevision().get()));
+        TreeWalk tw =
+            TreeWalk.forPath(rw.getObjectReader(), rsrc.getPatchKey().get(),
+                commit.getTree().getId());
+        if (tw == null) {
+          throw new ResourceNotFoundException();
+        }
+        try {
+          final ObjectLoader object = repo.open(tw.getObjectId(0));
+          return new BinaryResult() {
+            @Override
+            public void writeTo(OutputStream os) throws IOException {
+              object.copyTo(os);
+            }
+          }.setContentLength(object.getSize())
+           .base64();
+        } finally {
+          tw.release();
+        }
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
index b3cd813..936edd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -19,20 +19,32 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.kohsuke.args4j.Option;
+
 public class GetDetail implements RestReadView<ChangeResource> {
-  private final ChangeJson json;
+  private final GetChange delegate;
+
+  @Option(name = "-o", multiValued = true, usage = "Output options")
+  void addOption(ListChangesOption o) {
+    delegate.addOption(o);
+  }
+
+  @Option(name = "-O", usage = "Output option flags, in hex")
+  void setOptionFlagsHex(String hex) {
+    delegate.setOptionFlagsHex(hex);
+  }
 
   @Inject
-  GetDetail(ChangeJson json) {
-    this.json = json
-        .addOption(ListChangesOption.LABELS)
-        .addOption(ListChangesOption.DETAILED_LABELS)
-        .addOption(ListChangesOption.DETAILED_ACCOUNTS)
-        .addOption(ListChangesOption.MESSAGES);
+  GetDetail(GetChange delegate) {
+    this.delegate = delegate;
+    delegate.addOption(ListChangesOption.LABELS);
+    delegate.addOption(ListChangesOption.DETAILED_LABELS);
+    delegate.addOption(ListChangesOption.DETAILED_ACCOUNTS);
+    delegate.addOption(ListChangesOption.MESSAGES);
   }
 
   @Override
   public Object apply(ChangeResource rsrc) throws OrmException {
-    return json.format(rsrc);
+    return delegate.apply(rsrc);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
new file mode 100644
index 0000000..753a70b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -0,0 +1,346 @@
+// 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.change;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.prettify.common.SparseFileContent;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.patch.PatchScriptFactory;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.git.LargeObjectException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.diff.ReplaceEdit;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.NamedOptionDef;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class GetDiff implements RestReadView<FileResource> {
+  private final PatchScriptFactory.Factory patchScriptFactoryFactory;
+  private final Provider<Revisions> revisions;
+
+  @Option(name = "--base", metaVar = "REVISION")
+  String base;
+
+  @Option(name = "--ignore-whitespace")
+  IgnoreWhitespace ignoreWhitespace = IgnoreWhitespace.NONE;
+
+  @Option(name = "--context", handler = ContextOptionHandler.class)
+  short context = AccountDiffPreference.DEFAULT_CONTEXT;
+
+  @Option(name = "--intraline")
+  boolean intraline;
+
+  @Inject
+  GetDiff(PatchScriptFactory.Factory patchScriptFactoryFactory,
+      Provider<Revisions> revisions) {
+    this.patchScriptFactoryFactory = patchScriptFactoryFactory;
+    this.revisions = revisions;
+  }
+
+  @Override
+  public Object apply(FileResource resource)
+      throws OrmException, NoSuchChangeException, LargeObjectException, ResourceNotFoundException {
+    PatchSet.Id basePatchSet = null;
+    if (base != null) {
+      RevisionResource baseResource = revisions.get().parse(
+          resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
+      basePatchSet = baseResource.getPatchSet().getId();
+    }
+    AccountDiffPreference prefs = new AccountDiffPreference(new Account.Id(0));
+    prefs.setIgnoreWhitespace(ignoreWhitespace.whitespace);
+    prefs.setContext(context);
+    prefs.setIntralineDifference(intraline);
+
+    PatchScript ps = patchScriptFactoryFactory.create(
+        resource.getRevision().getControl(),
+        resource.getPatchKey().getFileName(),
+        basePatchSet,
+        resource.getPatchKey().getParentKey(),
+        prefs)
+          .call();
+
+    Content content = new Content(ps);
+    for (Edit edit : ps.getEdits()) {
+      if (edit.getType() == Edit.Type.EMPTY) {
+        continue;
+      }
+      content.addCommon(edit.getBeginA());
+
+      checkState(content.nextA == edit.getBeginA(),
+          "nextA = %d; want %d", content.nextA, edit.getBeginA());
+      checkState(content.nextB == edit.getBeginB(),
+          "nextB = %d; want %d", content.nextB, edit.getBeginB());
+      switch (edit.getType()) {
+        case DELETE:
+        case INSERT:
+        case REPLACE:
+          List<Edit> internalEdit = edit instanceof ReplaceEdit
+            ? ((ReplaceEdit) edit).getInternalEdits()
+            : null;
+          content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit);
+          break;
+        case EMPTY:
+        default:
+          throw new IllegalStateException();
+      }
+    }
+    content.addCommon(ps.getA().size());
+
+    Result result = new Result();
+    if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
+      result.metaA = new FileMeta();
+      result.metaA.name = Objects.firstNonNull(ps.getOldName(), ps.getNewName());
+      result.metaA.setContentType(ps.getFileModeA(), ps.getMimeTypeA());
+    }
+
+    if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
+      result.metaB = new FileMeta();
+      result.metaB.name = ps.getNewName();
+      result.metaB.setContentType(ps.getFileModeB(), ps.getMimeTypeB());
+    }
+
+    if (intraline) {
+      if (ps.hasIntralineTimeout()) {
+        result.intralineStatus = IntraLineStatus.TIMEOUT;
+      } else if (ps.hasIntralineFailure()) {
+        result.intralineStatus = IntraLineStatus.FAILURE;
+      } else {
+        result.intralineStatus = IntraLineStatus.OK;
+      }
+    }
+
+    result.changeType = ps.getChangeType();
+    if (ps.getPatchHeader().size() > 0) {
+      result.diffHeader = ps.getPatchHeader();
+    }
+    result.content = content.lines;
+    Response<Result> r = Response.ok(result);
+    if (resource.isCacheable()) {
+      r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+    }
+    return r;
+  }
+
+  static class Result {
+    FileMeta metaA;
+    FileMeta metaB;
+    IntraLineStatus intralineStatus;
+    ChangeType changeType;
+    List<String> diffHeader;
+    List<ContentEntry> content;
+  }
+
+  static class FileMeta {
+    String name;
+    String contentType;
+    String url;
+
+    void setContentType(FileMode fileMode, String mimeType) {
+      switch (fileMode) {
+        case FILE:
+          contentType = mimeType;
+          break;
+        case GITLINK:
+          contentType = "x-git/gitlink";
+          break;
+        case SYMLINK:
+          contentType = "x-git/symlink";
+          break;
+        default:
+          throw new IllegalStateException("file mode: " + fileMode);
+      }
+    }
+  }
+
+  enum IntraLineStatus {
+    OK,
+    TIMEOUT,
+    FAILURE;
+  }
+
+  private static class Content {
+    final List<ContentEntry> lines;
+    final SparseFileContent fileA;
+    final SparseFileContent fileB;
+
+    int nextA;
+    int nextB;
+
+    Content(PatchScript ps) {
+      lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2);
+      fileA = ps.getA();
+      fileB = ps.getB();
+    }
+
+    void addCommon(int end) {
+      end = Math.min(end, fileA.size());
+      if (nextA >= end) {
+        return;
+      }
+      nextB += end - nextA;
+
+      while (nextA < end) {
+        if (fileA.contains(nextA)) {
+          ContentEntry e = entry();
+          e.ab = Lists.newArrayListWithCapacity(end - nextA);
+          for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++) {
+            e.ab.add(fileA.get(i));
+          }
+        } else {
+          int endRegion = Math.min(end,
+              (nextA == 0) ? fileA.first() : fileA.next(nextA - 1));
+          ContentEntry e = entry();
+          e.skip = endRegion - nextA;
+          nextA = endRegion;
+        }
+      }
+    }
+
+    void addDiff(int endA, int endB, List<Edit> internalEdit) {
+      int lenA = endA - nextA;
+      int lenB = endB - nextB;
+      checkState(lenA > 0 || lenB > 0);
+
+      ContentEntry e = entry();
+      if (lenA > 0) {
+        e.a = Lists.newArrayListWithCapacity(lenA);
+        for (; nextA < endA; nextA++) {
+          e.a.add(fileA.get(nextA));
+        }
+      }
+      if (lenB > 0) {
+        e.b = Lists.newArrayListWithCapacity(lenB);
+        for (; nextB < endB; nextB++) {
+          e.b.add(fileB.get(nextB));
+        }
+      }
+      if (internalEdit != null && !internalEdit.isEmpty()) {
+        e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
+        e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
+        int lastA = 0;
+        int lastB = 0;
+        for (Edit edit : internalEdit) {
+          if (edit.getBeginA() != edit.getEndA()) {
+            e.editA.add(ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
+            lastA = edit.getEndA();
+          }
+          if (edit.getBeginB() != edit.getEndB()) {
+            e.editB.add(ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
+            lastB = edit.getEndB();
+          }
+        }
+      }
+    }
+
+    private ContentEntry entry() {
+      ContentEntry e = new ContentEntry();
+      lines.add(e);
+      return e;
+    }
+  }
+
+  enum IgnoreWhitespace {
+    NONE(AccountDiffPreference.Whitespace.IGNORE_NONE),
+    TRAILING(AccountDiffPreference.Whitespace.IGNORE_SPACE_AT_EOL),
+    CHANGED(AccountDiffPreference.Whitespace.IGNORE_SPACE_CHANGE),
+    ALL(AccountDiffPreference.Whitespace.IGNORE_ALL_SPACE);
+
+    private final AccountDiffPreference.Whitespace whitespace;
+
+    private IgnoreWhitespace(AccountDiffPreference.Whitespace whitespace) {
+      this.whitespace = whitespace;
+    }
+  }
+
+  static final class ContentEntry {
+    // Common lines to both sides.
+    List<String> ab;
+    // Lines of a.
+    List<String> a;
+    // Lines of b.
+    List<String> b;
+
+    // A list of changed sections of the of the corresponding line list.
+    // Each entry is a character <offset, length> pair. The offset is from the
+    // beginning of the first line in the list. Also, the offset includes an
+    // implied trailing newline character for each line.
+    List<List<Integer>> editA;
+    List<List<Integer>> editB;
+
+    // Number of lines to skip on both sides.
+    Integer skip;
+  }
+
+  public static class ContextOptionHandler extends OptionHandler<Short> {
+    public ContextOptionHandler(
+        CmdLineParser parser, OptionDef option, Setter<Short> setter) {
+      super(parser, option, setter);
+    }
+
+    @Override
+    public final int parseArguments(final Parameters params)
+        throws CmdLineException {
+      final String value = params.getParameter(0);
+      short context;
+      if ("all".equalsIgnoreCase(value)) {
+        context = AccountDiffPreference.WHOLE_FILE_CONTEXT;
+      } else {
+        try {
+          context = Short.parseShort(value, 10);
+          if (context < 0) {
+            throw new NumberFormatException();
+          }
+        } catch (NumberFormatException e) {
+          throw new CmdLineException(owner,
+              String.format("\"%s\" is not a valid value for \"%s\"",
+                  value, ((NamedOptionDef) option).name()));
+        }
+      }
+      setter.addValue(context);
+      return 1;
+    }
+
+    @Override
+    public final String getDefaultMetaVariable() {
+      return "ALL|# LINES";
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
new file mode 100644
index 0000000..4c1994b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -0,0 +1,177 @@
+// 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.change;
+
+import static com.google.common.base.Charsets.UTF_8;
+
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class GetPatch implements RestReadView<RevisionResource> {
+  private final GitRepositoryManager repoManager;
+
+  @Option(name = "--zip")
+  private boolean zip;
+
+  @Option(name = "--download")
+  private boolean download;
+
+  @Inject
+  GetPatch(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public BinaryResult apply(RevisionResource rsrc)
+      throws ResourceNotFoundException, ResourceConflictException {
+    Project.NameKey project = rsrc.getControl().getProject().getNameKey();
+    boolean close = true;
+    try {
+      final Repository repo = repoManager.openRepository(project);
+      try {
+        final RevWalk rw = new RevWalk(repo);
+        try {
+          final RevCommit commit =
+              rw.parseCommit(ObjectId.fromString(rsrc.getPatchSet()
+                  .getRevision().get()));
+          RevCommit[] parents = commit.getParents();
+          if (parents.length > 1) {
+            throw new ResourceConflictException(
+                "Revision has more than 1 parent.");
+          } else if (parents.length == 0) {
+            throw new ResourceConflictException("Revision has no parent.");
+          }
+          final RevCommit base = parents[0];
+          rw.parseBody(base);
+
+          BinaryResult bin = new BinaryResult() {
+            @Override
+            public void writeTo(OutputStream out) throws IOException {
+              if (zip) {
+                ZipOutputStream zos = new ZipOutputStream(out);
+                ZipEntry e = new ZipEntry(fileName(rw, commit));
+                e.setTime(commit.getCommitTime() * 1000L);
+                zos.putNextEntry(e);
+                format(zos);
+                zos.closeEntry();
+                zos.finish();
+              } else {
+                format(out);
+              }
+            }
+
+            private void format(OutputStream out) throws IOException {
+              out.write(formatEmailHeader(commit).getBytes(UTF_8));
+              DiffFormatter fmt = new DiffFormatter(out);
+              fmt.setRepository(repo);
+              fmt.format(base.getTree(), commit.getTree());
+              fmt.flush();
+            }
+
+            @Override
+            public void close() throws IOException {
+              rw.release();
+              repo.close();
+            }
+          };
+
+          if (zip) {
+            bin.disableGzip()
+               .setContentType("application/zip")
+               .setAttachmentName(fileName(rw, commit) + ".zip");
+          } else {
+            bin.base64()
+               .setContentType("application/mbox")
+               .setAttachmentName(download
+                   ? fileName(rw, commit) + ".base64"
+                   : null);
+          }
+
+          close = false;
+          return bin;
+        } finally {
+          if (close) {
+            rw.release();
+          }
+        }
+      } finally {
+        if (close) {
+          repo.close();
+        }
+      }
+    } catch (IOException e) {
+      throw new ResourceNotFoundException();
+    }
+  }
+
+  private static String formatEmailHeader(RevCommit commit) {
+    StringBuilder b = new StringBuilder();
+    PersonIdent author = commit.getAuthorIdent();
+    String subject = commit.getShortMessage();
+    String msg = commit.getFullMessage().substring(subject.length());
+    if (msg.startsWith("\n\n")) {
+      msg = msg.substring(2);
+    }
+    b.append("From ").append(commit.getName())
+     .append(' ')
+     .append("Mon Sep 17 00:00:00 2001\n")
+     .append("From: ").append(author.getName())
+     .append(" <").append(author.getEmailAddress()).append(">\n")
+     .append("Date: ").append(formatDate(author)).append('\n')
+     .append("Subject: [PATCH] ").append(subject).append('\n')
+     .append('\n')
+     .append(msg);
+    if (!msg.endsWith("\n")) {
+     b.append('\n');
+    }
+    return b.append("---\n\n").toString();
+  }
+
+  private static String formatDate(PersonIdent author) {
+    SimpleDateFormat df = new SimpleDateFormat(
+        "EEE, dd MMM yyyy HH:mm:ss Z",
+        Locale.US);
+    df.setCalendar(Calendar.getInstance(author.getTimeZone(), Locale.US));
+    return df.format(author.getWhen());
+  }
+
+  private static String fileName(RevWalk rw, RevCommit commit)
+      throws IOException {
+    AbbreviatedObjectId id = rw.getObjectReader().abbreviate(commit, 8);
+    return id.name() + ".diff";
+  }
+}
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
new file mode 100644
index 0000000..3776b74
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -0,0 +1,300 @@
+// 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.change;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeJson.CommitInfo;
+import com.google.gerrit.server.change.ChangeJson.GitPerson;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class GetRelated implements RestReadView<RevisionResource> {
+  private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
+
+  private final GitRepositoryManager gitMgr;
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  GetRelated(GitRepositoryManager gitMgr, Provider<ReviewDb> db) {
+    this.gitMgr = gitMgr;
+    this.dbProvider = db;
+  }
+
+  @Override
+  public Object apply(RevisionResource rsrc)
+      throws RepositoryNotFoundException, IOException, OrmException {
+    Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
+    try {
+      Ref ref = git.getRef(rsrc.getChange().getDest().get());
+      RevWalk rw = new RevWalk(git);
+      try {
+        RelatedInfo info = new RelatedInfo();
+        info.changes = walk(rsrc, rw, ref);
+        return info;
+      } finally {
+        rw.release();
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
+      throws OrmException, IOException {
+    Map<Change.Id, Change> changes = allOpenChanges(rsrc);
+    Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(changes.keySet());
+    List<ChangeAndCommit> list = children(rsrc, rw, changes, patchSets);
+
+    Map<String, PatchSet> commits = Maps.newHashMap();
+    for (PatchSet p : patchSets.values()) {
+      commits.put(p.getRevision().get(), p);
+    }
+
+    RevCommit rev = rw.parseCommit(ObjectId.fromString(
+        rsrc.getPatchSet().getRevision().get()));
+    rw.sort(RevSort.TOPO);
+    rw.markStart(rev);
+
+    if (ref != null && ref.getObjectId() != null) {
+      try {
+        rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
+      } catch (IncorrectObjectTypeException notCommit) {
+        // Ignore and treat as new branch.
+      }
+    }
+
+    for (RevCommit c; (c = rw.next()) != null;) {
+      PatchSet p = commits.get(c.name());
+      Change g = p != null ? changes.get(p.getId().getParentKey()) : null;
+      list.add(new ChangeAndCommit(g, p, c));
+    }
+
+    if (list.size() == 1) {
+      ChangeAndCommit r = list.get(0);
+      if (r._changeNumber != null && r._revisionNumber != null
+          && r._changeNumber == rsrc.getChange().getChangeId()
+          && r._revisionNumber == rsrc.getPatchSet().getPatchSetId()) {
+        return Collections.emptyList();
+      }
+    }
+    return list;
+  }
+
+  private Map<Change.Id, Change> allOpenChanges(RevisionResource rsrc)
+      throws OrmException {
+    ReviewDb db = dbProvider.get();
+    return db.changes().toMap(
+        db.changes().byBranchOpenAll(rsrc.getChange().getDest()));
+  }
+
+  private Map<PatchSet.Id, PatchSet> allPatchSets(Collection<Change.Id> ids)
+      throws OrmException {
+    int n = ids.size();
+    ReviewDb db = dbProvider.get();
+    List<ResultSet<PatchSet>> t = Lists.newArrayListWithCapacity(n);
+    for (Change.Id id : ids) {
+      t.add(db.patchSets().byChange(id));
+    }
+
+    Map<PatchSet.Id, PatchSet> r = Maps.newHashMapWithExpectedSize(n * 2);
+    for (ResultSet<PatchSet> rs : t) {
+      for (PatchSet p : rs) {
+        r.put(p.getId(), p);
+      }
+    }
+    return r;
+  }
+
+  private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
+      Map<Change.Id, Change> changes, Map<PatchSet.Id, PatchSet> patchSets)
+      throws OrmException, IOException {
+    // children is a map of parent commit name to PatchSet built on it.
+    Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
+
+    RevFlag seenCommit = rw.newFlag("seenCommit");
+    LinkedList<String> q = Lists.newLinkedList();
+    seedQueue(rsrc, rw, seenCommit, patchSets, q);
+
+    ProjectControl projectCtl = rsrc.getControl().getProjectControl();
+    Set<Change.Id> seenChange = Sets.newHashSet();
+    List<ChangeAndCommit> graph = Lists.newArrayList();
+    while (!q.isEmpty()) {
+      String id = q.remove();
+
+      // For every matching change find the most recent patch set.
+      Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
+      for (PatchSet.Id psId : children.get(id)) {
+        PatchSet.Id e = matches.get(psId.getParentKey());
+        if ((e == null || e.get() < psId.get())
+            && isVisible(projectCtl, changes, patchSets, psId))  {
+          matches.put(psId.getParentKey(), psId);
+        }
+      }
+
+      for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
+        Change change = changes.get(e.getKey());
+        PatchSet ps = patchSets.get(e.getValue());
+        if (change == null || ps == null || !seenChange.add(e.getKey())) {
+          continue;
+        }
+
+        RevCommit c = rw.parseCommit(ObjectId.fromString(
+            ps.getRevision().get()));
+        if (!c.has(seenCommit)) {
+          c.add(seenCommit);
+          q.addFirst(ps.getRevision().get());
+          graph.add(new ChangeAndCommit(change, ps, c));
+        }
+      }
+    }
+    Collections.reverse(graph);
+    return graph;
+  }
+
+  private boolean isVisible(ProjectControl projectCtl,
+      Map<Change.Id, Change> changes,
+      Map<PatchSet.Id, PatchSet> patchSets,
+      PatchSet.Id psId) throws OrmException {
+    Change c = changes.get(psId.getParentKey());
+    PatchSet ps = patchSets.get(psId);
+    if (c != null && ps != null) {
+      ChangeControl ctl = projectCtl.controlFor(c);
+      return ctl.isVisible(dbProvider.get())
+          && ctl.isPatchVisible(ps, dbProvider.get());
+    }
+    return false;
+  }
+
+  private void seedQueue(RevisionResource rsrc, RevWalk rw,
+      RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
+      LinkedList<String> q) throws IOException {
+    RevCommit tip = rw.parseCommit(ObjectId.fromString(
+        rsrc.getPatchSet().getRevision().get()));
+    tip.add(seenCommit);
+    q.add(tip.name());
+
+    Change.Id cId = rsrc.getChange().getId();
+    for (PatchSet p : patchSets.values()) {
+      if (cId.equals(p.getId().getParentKey())) {
+        try {
+          RevCommit c = rw.parseCommit(ObjectId.fromString(
+              rsrc.getPatchSet().getRevision().get()));
+          if (!c.has(seenCommit)) {
+            c.add(seenCommit);
+            q.add(c.name());
+          }
+        } catch (IOException e) {
+          log.warn(String.format(
+              "Cannot read patch set %d of %d",
+              p.getPatchSetId(), cId.get()), e);
+        }
+      }
+    }
+  }
+
+  private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
+      throws OrmException {
+    ReviewDb db = dbProvider.get();
+    List<ResultSet<PatchSetAncestor>> t =
+        Lists.newArrayListWithCapacity(ids.size());
+    for (Change.Id id : ids) {
+      t.add(db.patchSetAncestors().byChange(id));
+    }
+
+    Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
+    for (ResultSet<PatchSetAncestor> rs : t) {
+      for (PatchSetAncestor a : rs) {
+        r.put(a.getAncestorRevision().get(), a.getPatchSet());
+      }
+    }
+    return r;
+  }
+
+  private static GitPerson toGitPerson(PersonIdent id) {
+    GitPerson p = new GitPerson();
+    p.name = id.getName();
+    p.email = id.getEmailAddress();
+    p.date = new Timestamp(id.getWhen().getTime());
+    p.tz = id.getTimeZoneOffset();
+    return p;
+  }
+
+  static class RelatedInfo {
+    List<ChangeAndCommit> changes;
+  }
+
+  static class ChangeAndCommit {
+    String changeId;
+    CommitInfo commit;
+    Integer _changeNumber;
+    Integer _revisionNumber;
+
+    ChangeAndCommit(@Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
+      if (change != null) {
+        changeId = change.getKey().get();
+        _changeNumber = change.getChangeId();
+        _revisionNumber = ps != null ? ps.getPatchSetId() : null;
+      }
+
+      commit = new CommitInfo();
+      commit.commit = c.name();
+      commit.parents = Lists.newArrayListWithCapacity(c.getParentCount());
+      for (int i = 0; i < c.getParentCount(); i++) {
+        CommitInfo p = new CommitInfo();
+        p.commit = c.getParent(i).name();
+        commit.parents.add(p);
+      }
+      commit.author = toGitPerson(c.getAuthorIdent());
+      commit.subject = c.getShortMessage();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
index 08186fe..3676a1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
@@ -20,17 +20,17 @@
 import com.google.inject.Inject;
 
 public class GetReview implements RestReadView<RevisionResource> {
-  private final ChangeJson json;
+  private final GetChange delegate;
 
   @Inject
-  GetReview(ChangeJson json) {
-    this.json = json
-        .addOption(ListChangesOption.DETAILED_LABELS)
-        .addOption(ListChangesOption.DETAILED_ACCOUNTS);
+  GetReview(GetChange delegate) {
+    this.delegate = delegate;
+    delegate.addOption(ListChangesOption.DETAILED_LABELS);
+    delegate.addOption(ListChangesOption.DETAILED_ACCOUNTS);
   }
 
   @Override
-  public Object apply(RevisionResource resource) throws OrmException {
-    return json.format(resource);
+  public Object apply(RevisionResource rsrc) throws OrmException {
+    return delegate.apply(rsrc);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
new file mode 100644
index 0000000..26e7846
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -0,0 +1,88 @@
+// 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.change;
+
+import com.google.gerrit.common.data.IncludedInDetail;
+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.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.Collection;
+
+class IncludedIn implements RestReadView<ChangeResource> {
+
+  private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  IncludedIn(ReviewDb db, GitRepositoryManager repoManager) {
+    this.db = db;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public Object apply(ChangeResource rsrc) throws OrmException, IOException,
+      BadRequestException, ResourceConflictException {
+    ChangeControl ctl = rsrc.getControl();
+    PatchSet ps =
+        db.patchSets().get(ctl.getChange().currentPatchSetId());
+    Repository r =
+        repoManager.openRepository(ctl.getProject().getNameKey());
+    try {
+      RevWalk rw = new RevWalk(r);
+      try {
+        rw.setRetainBody(false);
+        RevCommit rev;
+        try {
+          rev = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+        } catch (IncorrectObjectTypeException err) {
+          throw new BadRequestException(err.getMessage());
+        } catch (MissingObjectException err) {
+          throw new ResourceConflictException(err.getMessage());
+        }
+        return new IncludedInInfo(IncludedInResolver.resolve(r, rw, rev));
+      } finally {
+        rw.release();
+      }
+    } finally {
+      r.close();
+    }
+  }
+
+  static class IncludedInInfo {
+    String kind = "gerritcodereview#includedininfo";
+    Collection<String> branches;
+    Collection<String> tags;
+
+    IncludedInInfo(IncludedInDetail in) {
+      branches = in.getBranches();
+      tags = in.getTags();
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
new file mode 100644
index 0000000..b02f6f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -0,0 +1,159 @@
+// 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.change;
+
+import com.google.gerrit.common.data.IncludedInDetail;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Resolve in which tags and branches a commit is included.
+ */
+public class IncludedInResolver {
+
+  private static final Logger log = LoggerFactory
+      .getLogger(IncludedInResolver.class);
+
+  public static IncludedInDetail resolve(final Repository repo,
+      final RevWalk rw, final RevCommit commit) throws IOException {
+
+    Set<Ref> tags =
+        new HashSet<Ref>(repo.getRefDatabase().getRefs(Constants.R_TAGS)
+            .values());
+    Set<Ref> branches =
+        new HashSet<Ref>(repo.getRefDatabase().getRefs(Constants.R_HEADS)
+            .values());
+    Set<Ref> allTagsAndBranches = new HashSet<Ref>();
+    allTagsAndBranches.addAll(tags);
+    allTagsAndBranches.addAll(branches);
+    Set<Ref> allMatchingTagsAndBranches =
+        includedIn(repo, rw, commit, allTagsAndBranches);
+
+    IncludedInDetail detail = new IncludedInDetail();
+    detail
+        .setBranches(getMatchingRefNames(allMatchingTagsAndBranches, branches));
+    detail.setTags(getMatchingRefNames(allMatchingTagsAndBranches, tags));
+
+    return detail;
+  }
+
+  /**
+   * Resolves which tip refs include the target commit.
+   */
+  private static Set<Ref> includedIn(final Repository repo, final RevWalk rw,
+      final RevCommit target, final Set<Ref> tipRefs) throws IOException,
+      MissingObjectException, IncorrectObjectTypeException {
+
+    Set<Ref> result = new HashSet<Ref>();
+
+    Map<RevCommit, Set<Ref>> tipsAndCommits = parseCommits(repo, rw, tipRefs);
+
+    List<RevCommit> tips = new ArrayList<RevCommit>(tipsAndCommits.keySet());
+    Collections.sort(tips, new Comparator<RevCommit>() {
+      @Override
+      public int compare(RevCommit c1, RevCommit c2) {
+        return c1.getCommitTime() - c2.getCommitTime();
+      }
+    });
+
+    Set<RevCommit> targetReachableFrom = new HashSet<RevCommit>();
+    targetReachableFrom.add(target);
+
+    for (RevCommit tip : tips) {
+      boolean commitFound = false;
+      rw.resetRetain(RevFlag.UNINTERESTING);
+      rw.markStart(tip);
+      for (RevCommit commit : rw) {
+        if (targetReachableFrom.contains(commit)) {
+          commitFound = true;
+          targetReachableFrom.add(tip);
+          result.addAll(tipsAndCommits.get(tip));
+          break;
+        }
+      }
+      if (!commitFound) {
+        rw.markUninteresting(tip);
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * Returns the short names of refs which are as well in the matchingRefs list
+   * as well as in the allRef list.
+   */
+  private static List<String> getMatchingRefNames(Set<Ref> matchingRefs,
+      Set<Ref> allRefs) {
+    List<String> refNames = new ArrayList<String>();
+    for (Ref matchingRef : matchingRefs) {
+      if (allRefs.contains(matchingRef)) {
+        refNames.add(Repository.shortenRefName(matchingRef.getName()));
+      }
+    }
+    return refNames;
+  }
+
+  /**
+   * Parse commit of ref and store the relation between ref and commit.
+   */
+  private static Map<RevCommit, Set<Ref>> parseCommits(final Repository repo,
+      final RevWalk rw, final Set<Ref> refs) throws IOException {
+    Map<RevCommit, Set<Ref>> result = new HashMap<RevCommit, Set<Ref>>();
+    for (Ref ref : refs) {
+      final RevCommit commit;
+      try {
+        commit = rw.parseCommit(ref.getObjectId());
+      } catch (IncorrectObjectTypeException notCommit) {
+        // Its OK for a tag reference to point to a blob or a tree, this
+        // is common in the Linux kernel or git.git repository.
+        //
+        continue;
+      } catch (MissingObjectException notHere) {
+        // Log the problem with this branch, but keep processing.
+        //
+        log.warn("Reference " + ref.getName() + " in " + repo.getDirectory()
+            + " points to dangling object " + ref.getObjectId());
+        continue;
+      }
+      Set<Ref> relatedRefs = result.get(commit);
+      if (relatedRefs == null) {
+        relatedRefs = new HashSet<Ref>();
+        result.put(commit, relatedRefs);
+      }
+      relatedRefs.add(ref);
+    }
+    return result;
+  }
+}
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
new file mode 100644
index 0000000..6ecc544
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -0,0 +1,44 @@
+// 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.change;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.Index.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.inject.Inject;
+
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class Index implements RestModifyView<ChangeResource, Input> {
+  public static class Input {
+  }
+
+  private final ChangeIndexer indexer;
+
+  @Inject
+  Index(ChangeIndexer indexer) {
+    this.indexer = indexer;
+  }
+
+  @Override
+  public Object apply(ChangeResource rsrc, Input input) throws IOException {
+    indexer.index(rsrc.getChange());
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
index 47863ac..97c7694 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -25,8 +26,6 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.change.CommentInfo;
-import com.google.gerrit.server.change.CommentInfo.Side;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
new file mode 100644
index 0000000..595e1c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -0,0 +1,214 @@
+// 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.change;
+
+import com.google.common.collect.Sets;
+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.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.CodeReviewCommit;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.SubmitStrategyFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+public class Mergeable implements RestReadView<RevisionResource> {
+  private static final Logger log = LoggerFactory.getLogger(Mergeable.class);
+
+  public static class MergeableInfo {
+    public Project.SubmitType submitType;
+    public boolean mergeable;
+  }
+
+  private final TestSubmitType.Get submitType;
+  private final GitRepositoryManager gitManager;
+  private final SubmitStrategyFactory submitStrategyFactory;
+  private final Provider<ReviewDb> db;
+
+  @Inject
+  Mergeable(TestSubmitType.Get submitType,
+      GitRepositoryManager gitManager,
+      SubmitStrategyFactory submitStrategyFactory,
+      Provider<ReviewDb> db) {
+    this.submitType = submitType;
+    this.gitManager = gitManager;
+    this.submitStrategyFactory = submitStrategyFactory;
+    this.db = db;
+  }
+
+  @Override
+  public MergeableInfo apply(RevisionResource resource)
+      throws ResourceConflictException, BadRequestException, AuthException,
+      OrmException, RepositoryNotFoundException, IOException {
+    Change change = resource.getChange();
+    PatchSet ps = resource.getPatchSet();
+    MergeableInfo result = new MergeableInfo();
+
+    if (!change.getStatus().isOpen()) {
+      throw new ResourceConflictException("change is " + Submit.status(change));
+    } else if (!ps.getId().equals(change.currentPatchSetId())) {
+      // Only the current revision is mergeable. Others always fail.
+      return result;
+    }
+
+    result.submitType = submitType.apply(resource);
+    result.mergeable = change.isMergeable();
+
+    Repository git = gitManager.openRepository(change.getProject());
+    try {
+      Map<String, Ref> refs = git.getRefDatabase().getRefs(RefDatabase.ALL);
+      Ref ref = refs.get(change.getDest().get());
+      if (isStale(change, ref)) {
+        result.mergeable =
+            refresh(change, ps, result.submitType, git, refs, ref);
+      }
+    } finally {
+      git.close();
+    }
+    return result;
+  }
+
+  private static boolean isStale(Change change, Ref ref) {
+    return change.getLastSha1MergeTested() == null
+        || !toRevId(ref).equals(change.getLastSha1MergeTested());
+  }
+
+  private static RevId toRevId(Ref ref) {
+    return new RevId(ref != null && ref.getObjectId() != null
+        ? ref.getObjectId().name()
+        : "");
+  }
+
+  private boolean refresh(Change change,
+      PatchSet ps,
+      Project.SubmitType type,
+      Repository git,
+      Map<String, Ref> refs,
+      Ref ref) throws IOException, OrmException {
+    RevWalk rw = new RevWalk(git) {
+      @Override
+      protected CodeReviewCommit createCommit(AnyObjectId id) {
+        return new CodeReviewCommit(id);
+      }
+    };
+    try {
+      ObjectId id;
+      try {
+        id = ObjectId.fromString(ps.getRevision().get());
+      } catch (IllegalArgumentException e) {
+        log.error(String.format(
+            "Invalid revision on patch set %d of %d",
+            ps.getId().get(),
+            change.getId().get()));
+        return false;
+      }
+
+      RevFlag canMerge = rw.newFlag("CAN_MERGE");
+      CodeReviewCommit rev = parse(rw, id);
+      rev.add(canMerge);
+
+      boolean mergeable;
+      if (ref == null || ref.getObjectId() == null) {
+        mergeable = true; // Assume yes on new branch.
+      } else {
+        CodeReviewCommit tip = parse(rw, ref.getObjectId());
+        Set<RevCommit> accepted = alreadyAccepted(rw, refs.values());
+        accepted.add(tip);
+        accepted.addAll(Arrays.asList(rev.getParents()));
+        mergeable = submitStrategyFactory.create(
+            type,
+            db.get(),
+            git,
+            rw,
+            null /*inserter*/,
+            canMerge,
+            accepted,
+            change.getDest()).dryRun(tip, rev);
+      }
+
+      Change c = db.get().changes().get(change.getId());
+      if (c != null) {
+        c.setMergeable(mergeable);
+        c.setLastSha1MergeTested(toRevId(ref));
+        db.get().changes().update(Collections.singleton(c));
+      }
+      return mergeable;
+    } catch (MergeException e) {
+      return false;
+    } catch (IOException e) {
+      log.error(String.format(
+          "Cannot merge test change %d", change.getId().get()), e);
+      return false;
+    } catch (NoSuchProjectException e) {
+      log.error(String.format(
+          "Cannot merge test change %d", change.getId().get()), e);
+      return false;
+    } finally {
+      rw.release();
+    }
+  }
+
+  private static Set<RevCommit> alreadyAccepted(RevWalk rw, Collection<Ref> refs)
+      throws MissingObjectException, IOException {
+    Set<RevCommit> accepted = Sets.newHashSet();
+    for (Ref r : refs) {
+      if (r.getName().startsWith(Constants.R_HEADS)
+          || r.getName().startsWith(Constants.R_TAGS)) {
+        try {
+          accepted.add(rw.parseCommit(r.getObjectId()));
+        } catch (IncorrectObjectTypeException nonCommit) {
+          // Not a commit? Skip over it.
+        }
+      }
+    }
+    return accepted;
+  }
+
+  private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+    return (CodeReviewCommit) rw.parseCommit(id);
+  }
+}
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 690faf3..9ed2bee 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
@@ -17,7 +17,7 @@
 import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
 import static com.google.gerrit.server.change.CommentResource.COMMENT_KIND;
 import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
-import static com.google.gerrit.server.change.PatchResource.PATCH_KIND;
+import static com.google.gerrit.server.change.FileResource.FILE_KIND;
 import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
 import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
 
@@ -31,38 +31,54 @@
 public class Module extends RestApiModule {
   @Override
   protected void configure() {
+    bind(ChangesCollection.class);
     bind(Revisions.class);
     bind(Reviewers.class);
     bind(Drafts.class);
     bind(Comments.class);
-    bind(Patches.class);
+    bind(Files.class);
 
     DynamicMap.mapOf(binder(), CHANGE_KIND);
     DynamicMap.mapOf(binder(), COMMENT_KIND);
     DynamicMap.mapOf(binder(), DRAFT_KIND);
-    DynamicMap.mapOf(binder(), PATCH_KIND);
+    DynamicMap.mapOf(binder(), FILE_KIND);
     DynamicMap.mapOf(binder(), REVIEWER_KIND);
     DynamicMap.mapOf(binder(), REVISION_KIND);
 
     get(CHANGE_KIND).to(GetChange.class);
     get(CHANGE_KIND, "detail").to(GetDetail.class);
     get(CHANGE_KIND, "topic").to(GetTopic.class);
+    get(CHANGE_KIND, "in").to(IncludedIn.class);
     put(CHANGE_KIND, "topic").to(PutTopic.class);
     delete(CHANGE_KIND, "topic").to(PutTopic.class);
+    delete(CHANGE_KIND).to(DeleteDraftChange.class);
     post(CHANGE_KIND, "abandon").to(Abandon.class);
+    post(CHANGE_KIND, "publish").to(Publish.CurrentRevision.class);
     post(CHANGE_KIND, "restore").to(Restore.class);
     post(CHANGE_KIND, "revert").to(Revert.class);
     post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
+    post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
+    post(CHANGE_KIND, "index").to(Index.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
+    get(CHANGE_KIND, "suggest_reviewers").to(SuggestReviewers.class);
     child(CHANGE_KIND, "reviewers").to(Reviewers.class);
     get(REVIEWER_KIND).to(GetReviewer.class);
     delete(REVIEWER_KIND).to(DeleteReviewer.class);
 
     child(CHANGE_KIND, "revisions").to(Revisions.class);
+    post(REVISION_KIND, "cherrypick").to(CherryPick.class);
+    get(REVISION_KIND, "commit").to(GetCommit.class);
+    delete(REVISION_KIND).to(DeleteDraftPatchSet.class);
+    get(REVISION_KIND, "mergeable").to(Mergeable.class);
+    post(REVISION_KIND, "publish").to(Publish.class);
+    get(REVISION_KIND, "related").to(GetRelated.class);
     get(REVISION_KIND, "review").to(GetReview.class);
     post(REVISION_KIND, "review").to(PostReview.class);
     post(REVISION_KIND, "submit").to(Submit.class);
+    post(REVISION_KIND, "rebase").to(Rebase.class);
+    post(REVISION_KIND, "message").to(EditMessage.class);
+    get(REVISION_KIND, "patch").to(GetPatch.class);
     get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
     post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
     post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
@@ -76,9 +92,11 @@
     child(REVISION_KIND, "comments").to(Comments.class);
     get(COMMENT_KIND).to(GetComment.class);
 
-    child(REVISION_KIND, "files").to(Patches.class);
-    put(PATCH_KIND, "reviewed").to(PutReviewed.class);
-    delete(PATCH_KIND, "reviewed").to(DeleteReviewed.class);
+    child(REVISION_KIND, "files").to(Files.class);
+    put(FILE_KIND, "reviewed").to(PutReviewed.class);
+    delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
+    get(FILE_KIND, "content").to(GetContent.class);
+    get(FILE_KIND, "diff").to(GetDiff.class);
 
     install(new FactoryModule() {
       @Override
@@ -86,6 +104,8 @@
         factory(ReviewerResource.Factory.class);
         factory(AccountInfo.Loader.Factory.class);
         factory(EmailReviewComments.Factory.class);
+        factory(ChangeInserter.Factory.class);
+        factory(PatchSetInserter.Factory.class);
       }
     });
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
deleted file mode 100644
index d3cf5c6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
+++ /dev/null
@@ -1,42 +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.change;
-
-import com.google.gerrit.extensions.restapi.RestResource;
-import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.inject.TypeLiteral;
-
-public class PatchResource implements RestResource {
-  public static final TypeLiteral<RestView<PatchResource>> PATCH_KIND =
-      new TypeLiteral<RestView<PatchResource>>() {};
-
-  private final RevisionResource rev;
-  private final Patch.Key key;
-
-  PatchResource(RevisionResource rev, String name) {
-    this.rev = rev;
-    this.key = new Patch.Key(rev.getPatchSet().getId(), name);
-  }
-
-  public Patch.Key getPatchKey() {
-    return key;
-  }
-
-  Account.Id getAccountId() {
-    return rev.getAccountId();
-  }
-}
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
new file mode 100644
index 0000000..e4f4f1f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -0,0 +1,419 @@
+// 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.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.gerrit.common.ChangeHooks;
+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.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.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MergeUtil;
+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.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.FooterLine;
+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;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class PatchSetInserter {
+  private static final Logger log =
+      LoggerFactory.getLogger(PatchSetInserter.class);
+
+  public static interface Factory {
+    PatchSetInserter create(Repository git, RevWalk revWalk, RefControl refControl,
+        IdentifiedUser user, Change change, RevCommit commit);
+  }
+
+  /**
+   * Whether to use {@link CommitValidators#validateForGerritCommits},
+   * {@link CommitValidators#validateForReceiveCommits}, or no commit
+   * validation.
+   */
+  public static enum ValidatePolicy {
+    GERRIT, RECEIVE_COMMITS, NONE;
+  }
+
+  public static enum ChangeKind {
+    REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE;
+  }
+
+  private final ChangeHooks hooks;
+  private final TrackingFooters trackingFooters;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ReviewDb db;
+  private final IdentifiedUser user;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final CommitValidators.Factory commitValidatorsFactory;
+  private final ChangeIndexer indexer;
+  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+  private final MergeUtil.Factory mergeUtilFactory;
+
+  private final Repository git;
+  private final RevWalk revWalk;
+  private final RevCommit commit;
+  private final Change change;
+  private final RefControl refControl;
+
+  private PatchSet patchSet;
+  private ChangeMessage changeMessage;
+  private boolean copyLabels;
+  private SshInfo sshInfo;
+  private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
+  private boolean draft;
+  private boolean runHooks;
+  private boolean sendMail;
+
+  @Inject
+  public PatchSetInserter(ChangeHooks hooks,
+      TrackingFooters trackingFooters,
+      ReviewDb db,
+      PatchSetInfoFactory patchSetInfoFactory,
+      GitReferenceUpdated gitRefUpdated,
+      CommitValidators.Factory commitValidatorsFactory,
+      ChangeIndexer indexer,
+      ReplacePatchSetSender.Factory replacePatchSetFactory,
+      MergeUtil.Factory mergeUtilFactory,
+      @Assisted Repository git,
+      @Assisted RevWalk revWalk,
+      @Assisted RefControl refControl,
+      @Assisted IdentifiedUser user,
+      @Assisted Change change,
+      @Assisted RevCommit commit) {
+    this.hooks = hooks;
+    this.trackingFooters = trackingFooters;
+    this.db = db;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.user = user;
+    this.gitRefUpdated = gitRefUpdated;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.indexer = indexer;
+    this.replacePatchSetFactory = replacePatchSetFactory;
+    this.mergeUtilFactory = mergeUtilFactory;
+
+    this.git = git;
+    this.revWalk = revWalk;
+    this.refControl = refControl;
+    this.change = change;
+    this.commit = commit;
+    this.runHooks = true;
+    this.sendMail = true;
+  }
+
+  public PatchSetInserter setPatchSet(PatchSet patchSet) {
+    PatchSet.Id psid = patchSet.getId();
+    checkArgument(psid.getParentKey().equals(change.getId()),
+        "patch set %s not for change %s", psid, change.getId());
+    checkArgument(psid.get() > change.currentPatchSetId().get(),
+        "new patch set ID %s is not greater than current patch set ID %s",
+        psid.get(), change.currentPatchSetId().get());
+    this.patchSet = patchSet;
+    return this;
+  }
+
+  public PatchSet.Id getPatchSetId() throws IOException {
+    init();
+    return patchSet.getId();
+  }
+
+  public PatchSetInserter setMessage(String message) throws OrmException {
+    changeMessage = new ChangeMessage(
+        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
+        user.getAccountId(), TimeUtil.nowTs(), patchSet.getId());
+    changeMessage.setMessage(message);
+    return this;
+  }
+
+  public PatchSetInserter setMessage(ChangeMessage changeMessage) throws OrmException {
+    this.changeMessage = changeMessage;
+    return this;
+  }
+
+  public PatchSetInserter setCopyLabels(boolean copyLabels) {
+    this.copyLabels = copyLabels;
+    return this;
+  }
+
+  public PatchSetInserter setSshInfo(SshInfo sshInfo) {
+    this.sshInfo = sshInfo;
+    return this;
+  }
+
+  public PatchSetInserter setValidatePolicy(ValidatePolicy validate) {
+    this.validatePolicy = checkNotNull(validate);
+    return this;
+  }
+
+  public PatchSetInserter setDraft(boolean draft) {
+    this.draft = draft;
+    return this;
+  }
+
+  public PatchSetInserter setRunHooks(boolean runHooks) {
+    this.runHooks = runHooks;
+    return this;
+  }
+
+  public PatchSetInserter setSendMail(boolean sendMail) {
+    this.sendMail = sendMail;
+    return this;
+  }
+
+  public Change insert() throws InvalidChangeOperationException, OrmException,
+      IOException {
+    init();
+    validate();
+
+    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(),
+          change.getDest().getParentKey().get(), ru.getResult()));
+    }
+    gitRefUpdated.fire(change.getProject(), ru);
+
+    final PatchSet.Id currentPatchSetId = change.currentPatchSetId();
+
+    db.changes().beginTransaction(change.getId());
+    try {
+      if (!db.changes().get(change.getId()).getStatus().isOpen()) {
+        throw new InvalidChangeOperationException(String.format(
+            "Change %s is closed", change.getId()));
+      }
+
+      ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+      db.patchSets().insert(Collections.singleton(patchSet));
+
+      final List<PatchSetApproval> oldPatchSetApprovals =
+          db.patchSetApprovals().byChange(change.getId()).toList();
+      final Set<Account.Id> oldReviewers = Sets.newHashSet();
+      final Set<Account.Id> oldCC = Sets.newHashSet();
+      for (PatchSetApproval a : oldPatchSetApprovals) {
+        if (a.getValue() != 0) {
+          oldReviewers.add(a.getAccountId());
+        } else {
+          oldCC.add(a.getAccountId());
+        }
+      }
+
+      updatedChange =
+          db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+            @Override
+            public Change update(Change change) {
+              if (change.getStatus().isClosed()) {
+                return null;
+              }
+              if (!change.currentPatchSetId().equals(currentPatchSetId)) {
+                return null;
+              }
+              if (change.getStatus() != Change.Status.DRAFT) {
+                change.setStatus(Change.Status.NEW);
+              }
+              change.setLastSha1MergeTested(null);
+              change.setCurrentPatchSet(patchSetInfoFactory.get(commit,
+                  patchSet.getId()));
+              ChangeUtil.updated(change);
+              return change;
+            }
+          });
+      if (updatedChange == null) {
+        throw new ChangeModifiedException(String.format(
+            "Change %s was modified", change.getId()));
+      }
+
+      if (copyLabels) {
+        PatchSet priorPatchSet = db.patchSets().get(currentPatchSetId);
+        ObjectId priorCommitId = ObjectId.fromString(priorPatchSet.getRevision().get());
+        RevCommit priorCommit = revWalk.parseCommit(priorCommitId);
+        ProjectState projectState =
+            refControl.getProjectControl().getProjectState();
+        ChangeKind changeKind =
+            getChangeKind(mergeUtilFactory, projectState, git, priorCommit, commit);
+
+        ApprovalsUtil.copyLabels(db, refControl.getProjectControl()
+            .getLabelTypes(), currentPatchSetId, patchSet, changeKind);
+      }
+
+      final List<FooterLine> footerLines = commit.getFooterLines();
+      ChangeUtil.updateTrackingIds(db, updatedChange, trackingFooters, footerLines);
+      db.commit();
+
+      if (changeMessage != null) {
+        db.changeMessages().insert(Collections.singleton(changeMessage));
+      }
+
+      if (sendMail) {
+        try {
+          PatchSetInfo info = patchSetInfoFactory.get(commit, patchSet.getId());
+          ReplacePatchSetSender cm =
+              replacePatchSetFactory.create(updatedChange);
+          cm.setFrom(user.getAccountId());
+          cm.setPatchSet(patchSet, info);
+          cm.setChangeMessage(changeMessage);
+          cm.addReviewers(oldReviewers);
+          cm.addExtraCC(oldCC);
+          cm.send();
+        } catch (Exception err) {
+          log.error("Cannot send email for new patch set on change " + updatedChange.getId(),
+              err);
+        }
+      }
+
+    } finally {
+      db.rollback();
+    }
+    CheckedFuture<?, IOException> e = indexer.indexAsync(updatedChange);
+    if (runHooks) {
+      hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
+    }
+    e.checkedGet();
+    return updatedChange;
+  }
+
+  private void init() throws IOException {
+    if (sshInfo == null) {
+      sshInfo = new NoSshInfo();
+    }
+    if (patchSet == null) {
+      patchSet = new PatchSet(
+          ChangeUtil.nextPatchSetId(git, change.currentPatchSetId()));
+      patchSet.setCreatedOn(TimeUtil.nowTs());
+      patchSet.setUploader(change.getOwner());
+      patchSet.setRevision(new RevId(commit.name()));
+    }
+    patchSet.setDraft(draft);
+  }
+
+  private void validate() throws InvalidChangeOperationException {
+    CommitValidators cv = commitValidatorsFactory.create(refControl, sshInfo, git);
+
+    String refName = patchSet.getRefName();
+    CommitReceivedEvent event = new CommitReceivedEvent(
+        new ReceiveCommand(
+            ObjectId.zeroId(),
+            commit.getId(),
+            refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
+        refControl.getProjectControl().getProject(), refControl.getRefName(),
+        commit, user);
+
+    try {
+      switch (validatePolicy) {
+      case RECEIVE_COMMITS:
+        cv.validateForReceiveCommits(event);
+        break;
+      case GERRIT:
+        cv.validateForGerritCommits(event);
+        break;
+      case NONE:
+        break;
+      }
+    } catch (CommitValidationException e) {
+      throw new InvalidChangeOperationException(e.getMessage());
+    }
+  }
+
+  public static ChangeKind getChangeKind(MergeUtil.Factory mergeUtilFactory, ProjectState project,
+      Repository git, RevCommit prior, RevCommit next) {
+    if (!next.getFullMessage().equals(prior.getFullMessage())) {
+      if (next.getTree() == prior.getTree()
+          && prior.getParent(0).equals(next.getParent(0))) {
+        return ChangeKind.NO_CODE_CHANGE;
+      } else {
+        return ChangeKind.REWORK;
+      }
+    }
+
+    if (prior.getParentCount() != 1 || next.getParentCount() != 1) {
+      // Trivial rebases done by machine only work well on 1 parent.
+      return ChangeKind.REWORK;
+    }
+
+    if (next.getTree() == prior.getTree() &&
+       prior.getParent(0).equals(next.getParent(0))) {
+      return ChangeKind.TRIVIAL_REBASE;
+    }
+
+    // A trivial rebase can be detected by looking for the next commit
+    // having the same tree as would exist when the prior commit is
+    // cherry-picked onto the next commit's new first parent.
+    try {
+      MergeUtil mergeUtil = mergeUtilFactory.create(project);
+      ThreeWayMerger merger =
+          mergeUtil.newThreeWayMerger(git, mergeUtil.createDryRunInserter());
+      merger.setBase(prior.getParent(0));
+      if (merger.merge(next.getParent(0), prior)
+          && merger.getResultTreeId().equals(next.getTree())) {
+        return ChangeKind.TRIVIAL_REBASE;
+      } else {
+        return ChangeKind.REWORK;
+      }
+    } catch (IOException err) {
+      log.warn("Cannot check trivial rebase of new patch set " + next.name()
+          + " in " + project.getProject().getName(), err);
+      return ChangeKind.REWORK;
+    }
+  }
+
+  public class ChangeModifiedException extends InvalidChangeOperationException {
+    private static final long serialVersionUID = 1L;
+
+    public ChangeModifiedException(String msg) {
+      super(msg);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java
deleted file mode 100644
index cb0a9bf..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ChildCollection;
-import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-class Patches implements ChildCollection<RevisionResource, PatchResource> {
-  private final DynamicMap<RestView<PatchResource>> views;
-
-  @Inject
-  Patches(DynamicMap<RestView<PatchResource>> views) {
-    this.views = views;
-  }
-
-  @Override
-  public DynamicMap<RestView<PatchResource>> views() {
-    return views;
-  }
-
-  @Override
-  public RestView<RevisionResource> list() throws AuthException {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public PatchResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException, AuthException {
-    return new PatchResource(rev, id.get());
-  }
-}
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 9c14bcb..367087c 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
@@ -20,7 +20,10 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.Futures;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
@@ -29,23 +32,30 @@
 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.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+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.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.change.PostReview.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.Iterator;
@@ -60,7 +70,7 @@
     public String message;
 
     public Map<String, Short> labels;
-    Map<String, List<Comment>> comments;
+    public Map<String, List<Comment>> comments;
 
     /**
      * If true require all labels to be within the user's permitted ranges based
@@ -80,6 +90,18 @@
 
     /** Who to send email notifications to after review is stored. */
     public NotifyHandling notify = NotifyHandling.ALL;
+
+    /**
+     * Account ID, name, email address or username of another user. The review
+     * will be posted/updated on behalf of this named user instead of the
+     * caller. Caller must have the labelAs-$NAME permission granted for each
+     * label that appears in {@link #labels}. This is in addition to the named
+     * user also needing to have permission to use the labels.
+     * <p>
+     * {@link #strictLabels} impacts how labels is processed for the named user,
+     * not the caller.
+     */
+    public String onBehalfOf;
   }
 
   public static enum DraftHandling {
@@ -90,19 +112,22 @@
     NONE, OWNER, OWNER_REVIEWERS, ALL;
   }
 
-  static class Comment {
-    String id;
-    CommentInfo.Side side;
-    int line;
-    String inReplyTo;
-    String message;
+  public static class Comment {
+    public String id;
+    public Side side;
+    public int line;
+    public String inReplyTo;
+    public String message;
+    public CommentRange range;
   }
 
   static class Output {
     Map<String, Short> labels;
   }
 
-  private final ReviewDb db;
+  private final Provider<ReviewDb> db;
+  private final ChangeIndexer indexer;
+  private final AccountsCollection accounts;
   private final EmailReviewComments.Factory email;
   @Deprecated private final ChangeHooks hooks;
 
@@ -114,17 +139,25 @@
   private Map<String, Short> categories = Maps.newHashMap();
 
   @Inject
-  PostReview(ReviewDb db,
+  PostReview(Provider<ReviewDb> db,
+      ChangeIndexer indexer,
+      AccountsCollection accounts,
       EmailReviewComments.Factory email,
       ChangeHooks hooks) {
     this.db = db;
+    this.indexer = indexer;
+    this.accounts = accounts;
     this.email = email;
     this.hooks = hooks;
   }
 
   @Override
   public Object apply(RevisionResource revision, Input input)
-      throws AuthException, BadRequestException, OrmException {
+      throws AuthException, BadRequestException, OrmException,
+      UnprocessableEntityException, IOException {
+    if (input.onBehalfOf != null) {
+      revision = onBehalfOf(revision, input);
+    }
     if (input.labels != null) {
       checkLabels(revision, input.strictLabels, input.labels);
     }
@@ -136,24 +169,30 @@
       input.notify = NotifyHandling.NONE;
     }
 
-    db.changes().beginTransaction(revision.getChange().getId());
+    db.get().changes().beginTransaction(revision.getChange().getId());
+    boolean dirty = false;
     try {
-      change = db.changes().get(revision.getChange().getId());
+      change = db.get().changes().get(revision.getChange().getId());
       ChangeUtil.updated(change);
       timestamp = change.getLastUpdatedOn();
 
-      boolean dirty = false;
       dirty |= insertComments(revision, input.comments, input.drafts);
       dirty |= updateLabels(revision, input.labels);
       dirty |= insertMessage(revision, input.message);
       if (dirty) {
-        db.changes().update(Collections.singleton(change));
-        db.commit();
+        db.get().changes().update(Collections.singleton(change));
+        db.get().commit();
       }
     } finally {
-      db.rollback();
+      db.get().rollback();
     }
 
+    CheckedFuture<?, IOException> indexWrite;
+    if (dirty) {
+      indexWrite = indexer.indexAsync(change);
+    } else {
+      indexWrite = Futures.<Void, IOException> immediateCheckedFuture(null);
+    }
     if (input.notify.compareTo(NotifyHandling.NONE) > 0 && message != null) {
       email.create(
           input.notify,
@@ -167,9 +206,49 @@
 
     Output output = new Output();
     output.labels = input.labels;
+    indexWrite.checkedGet();
     return output;
   }
 
+  private RevisionResource onBehalfOf(RevisionResource rev, Input in)
+      throws BadRequestException, AuthException, UnprocessableEntityException,
+      OrmException {
+    if (in.labels == null || in.labels.isEmpty()) {
+      throw new AuthException(String.format(
+          "label required to post review on behalf of \"%s\"",
+          in.onBehalfOf));
+    }
+
+    ChangeControl caller = rev.getControl();
+    Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
+    while (itr.hasNext()) {
+      Map.Entry<String, Short> ent = itr.next();
+      LabelType type = caller.getLabelTypes().byLabel(ent.getKey());
+      if (type == null && in.strictLabels) {
+        throw new BadRequestException(String.format(
+            "label \"%s\" is not a configured label", ent.getKey()));
+      } else if (type == null) {
+        itr.remove();
+        continue;
+      }
+
+      PermissionRange r = caller.getRange(Permission.forLabelAs(type.getName()));
+      if (r == null || r.isEmpty() || !r.contains(ent.getValue())) {
+        throw new AuthException(String.format(
+            "not permitted to modify label \"%s\" on behalf of \"%s\"",
+            ent.getKey(), in.onBehalfOf));
+      }
+    }
+    if (in.labels.isEmpty()) {
+      throw new AuthException(String.format(
+          "label required to post review on behalf of \"%s\"",
+          in.onBehalfOf));
+    }
+
+    ChangeControl target = caller.forUser(accounts.parse(in.onBehalfOf));
+    return new RevisionResource(new ChangeResource(target), rev.getPatchSet());
+  }
+
   private void checkLabels(RevisionResource revision, boolean strict,
       Map<String, Short> labels) throws BadRequestException, AuthException {
     ChangeControl ctl = revision.getControl();
@@ -280,17 +359,18 @@
           e = new PatchLineComment(
               new PatchLineComment.Key(
                   new Patch.Key(rsrc.getPatchSet().getId(), path),
-                  ChangeUtil.messageUUID(db)),
+                  ChangeUtil.messageUUID(db.get())),
               c.line,
               rsrc.getAccountId(),
-              parent);
+              parent, TimeUtil.nowTs());
         } else if (parent != null) {
           e.setParentUuid(parent);
         }
         e.setStatus(PatchLineComment.Status.PUBLISHED);
         e.setWrittenOn(timestamp);
-        e.setSide(c.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+        e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
         e.setMessage(c.message);
+        e.setRange(c.range);
         (create ? ins : upd).add(e);
       }
     }
@@ -310,9 +390,9 @@
         }
         break;
     }
-    db.patchComments().delete(del);
-    db.patchComments().insert(ins);
-    db.patchComments().update(upd);
+    db.get().patchComments().delete(del);
+    db.get().patchComments().insert(ins);
+    db.get().patchComments().update(upd);
     comments.addAll(ins);
     comments.addAll(upd);
     return !del.isEmpty() || !ins.isEmpty() || !upd.isEmpty();
@@ -321,7 +401,7 @@
   private Map<String, PatchLineComment> scanDraftComments(
       RevisionResource rsrc) throws OrmException {
     Map<String, PatchLineComment> drafts = Maps.newHashMap();
-    for (PatchLineComment c : db.patchComments().draftByPatchSetAuthor(
+    for (PatchLineComment c : db.get().patchComments().draftByPatchSetAuthor(
           rsrc.getPatchSet().getId(),
           rsrc.getAccountId())) {
       drafts.put(c.getKey().get(), c);
@@ -350,11 +430,12 @@
       }
 
       PatchSetApproval c = current.remove(name);
+      String normName = lt.getName();
       if (ent.getValue() == null || ent.getValue() == 0) {
         // User requested delete of this label.
         if (c != null) {
           if (c.getValue() != 0) {
-            labelDelta.add("-" + name);
+            labelDelta.add("-" + normName);
           }
           del.add(c);
         }
@@ -363,28 +444,28 @@
         c.setGranted(timestamp);
         c.cache(change);
         upd.add(c);
-        labelDelta.add(format(name, c.getValue()));
-        categories.put(name, c.getValue());
+        labelDelta.add(format(normName, c.getValue()));
+        categories.put(normName, c.getValue());
       } else if (c != null && c.getValue() == ent.getValue()) {
-        current.put(name, c);
+        current.put(normName, c);
       } else if (c == null) {
         c = new PatchSetApproval(new PatchSetApproval.Key(
                 rsrc.getPatchSet().getId(),
                 rsrc.getAccountId(),
                 lt.getLabelId()),
-            ent.getValue());
+            ent.getValue(), TimeUtil.nowTs());
         c.setGranted(timestamp);
         c.cache(change);
         ins.add(c);
-        labelDelta.add(format(name, c.getValue()));
-        categories.put(name, c.getValue());
+        labelDelta.add(format(normName, c.getValue()));
+        categories.put(normName, c.getValue());
       }
     }
 
     forceCallerAsReviewer(rsrc, current, ins, upd, del);
-    db.patchSetApprovals().delete(del);
-    db.patchSetApprovals().insert(ins);
-    db.patchSetApprovals().update(upd);
+    db.get().patchSetApprovals().delete(del);
+    db.get().patchSetApprovals().insert(ins);
+    db.get().patchSetApprovals().update(upd);
     return !del.isEmpty() || !ins.isEmpty() || !upd.isEmpty();
   }
 
@@ -401,7 +482,7 @@
             rsrc.getAccountId(),
             rsrc.getControl().getLabelTypes().getLabelTypes().get(0)
                 .getLabelId()),
-            (short) 0);
+            (short) 0, TimeUtil.nowTs());
         c.setGranted(timestamp);
         c.cache(change);
         ins.add(c);
@@ -422,7 +503,7 @@
       List<PatchSetApproval> del) throws OrmException {
     LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
     Map<String, PatchSetApproval> current = Maps.newHashMap();
-    for (PatchSetApproval a : db.patchSetApprovals().byPatchSetUser(
+    for (PatchSetApproval a : db.get().patchSetApprovals().byPatchSetUser(
           rsrc.getPatchSet().getId(), rsrc.getAccountId())) {
       if (a.isSubmit()) {
         continue;
@@ -469,7 +550,7 @@
     }
 
     message = new ChangeMessage(
-        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
+        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db.get())),
         rsrc.getAccountId(),
         timestamp,
         rsrc.getPatchSet().getId());
@@ -477,7 +558,7 @@
         "Patch Set %d:%s",
         rsrc.getPatchSet().getPatchSetId(),
         buf.toString()));
-    db.changeMessages().insert(Collections.singleton(message));
+    db.get().changeMessages().insert(Collections.singleton(message));
     return true;
   }
 
@@ -489,7 +570,7 @@
           user.getAccount(),
           rsrc.getPatchSet(),
           message.getMessage(),
-          categories, db);
+          categories, db.get());
     } catch (OrmException e) {
       log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
     }
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 70cf259..13deeb0 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
@@ -20,6 +20,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+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;
@@ -37,6 +38,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountInfo;
@@ -47,22 +49,30 @@
 import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.List;
 import java.util.Set;
 
 public class PostReviewers implements RestModifyView<ChangeResource, Input> {
-  public final static int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
-  public final static int DEFAULT_MAX_REVIEWERS = 20;
+  private static final Logger log = LoggerFactory
+      .getLogger(PostReviewers.class);
+
+  public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
+  public static final int DEFAULT_MAX_REVIEWERS = 20;
 
   public static class Input {
     @DefaultInput
@@ -80,13 +90,14 @@
   private final Provider<GroupsCollection> groupsCollection;
   private final GroupMembers.Factory groupMembersFactory;
   private final AccountInfo.Loader.Factory accountLoaderFactory;
-  private final Provider<ReviewDb> db;
+  private final Provider<ReviewDb> dbProvider;
   private final IdentifiedUser currentUser;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
   private final ChangeHooks hooks;
   private final AccountCache accountCache;
   private final ReviewerJson json;
+  private final ChangeIndexer indexer;
 
   @Inject
   PostReviewers(AccountsCollection accounts,
@@ -101,26 +112,28 @@
       @GerritServerConfig Config cfg,
       ChangeHooks hooks,
       AccountCache accountCache,
-      ReviewerJson json) {
+      ReviewerJson json,
+      ChangeIndexer indexer) {
     this.accounts = accounts;
     this.reviewerFactory = reviewerFactory;
     this.addReviewerSenderFactory = addReviewerSenderFactory;
     this.groupsCollection = groupsCollection;
     this.groupMembersFactory = groupMembersFactory;
     this.accountLoaderFactory = accountLoaderFactory;
-    this.db = db;
+    this.dbProvider = db;
     this.currentUser = currentUser;
     this.identifiedUserFactory = identifiedUserFactory;
     this.cfg = cfg;
     this.hooks = hooks;
     this.accountCache = accountCache;
     this.json = json;
+    this.indexer = indexer;
   }
 
   @Override
   public PostResult apply(ChangeResource rsrc, Input input)
       throws BadRequestException, ResourceNotFoundException, AuthException,
-      UnprocessableEntityException, OrmException, EmailException {
+      UnprocessableEntityException, OrmException, EmailException, IOException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
@@ -140,15 +153,15 @@
   }
 
   private PostResult putAccount(ReviewerResource rsrc) throws OrmException,
-      EmailException {
+      EmailException, IOException {
     PostResult result = new PostResult();
     addReviewers(rsrc, result, ImmutableSet.of(rsrc.getUser()));
     return result;
   }
 
   private PostResult putGroup(ChangeResource rsrc, Input input)
-      throws ResourceNotFoundException, AuthException, BadRequestException,
-      UnprocessableEntityException, OrmException, EmailException {
+      throws BadRequestException,
+      UnprocessableEntityException, OrmException, EmailException, IOException {
     GroupDescription.Basic group = groupsCollection.get().parseInternal(input.reviewer);
     PostResult result = new PostResult();
     if (!isLegalReviewerGroup(group.getGroupUUID())) {
@@ -208,15 +221,17 @@
   }
 
   private void addReviewers(ChangeResource rsrc, PostResult result,
-      Set<IdentifiedUser> reviewers) throws OrmException, EmailException {
+      Set<IdentifiedUser> reviewers)
+      throws OrmException, EmailException, IOException {
     if (reviewers.isEmpty()) {
       result.reviewers = ImmutableList.of();
       return;
     }
 
+    ReviewDb db = dbProvider.get();
     PatchSet.Id psid = rsrc.getChange().currentPatchSetId();
     Set<Account.Id> existing = Sets.newHashSet();
-    for (PatchSetApproval psa : db.get().patchSetApprovals().byPatchSet(psid)) {
+    for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(psid)) {
       existing.add(psa.getAccountId());
     }
 
@@ -234,9 +249,23 @@
           new ReviewerInfo(id), control, ImmutableList.of(psa)));
       toInsert.add(psa);
     }
-    db.get().patchSetApprovals().insert(toInsert);
+    if (toInsert.isEmpty()) {
+      return;
+    }
+
+    db.changes().beginTransaction(rsrc.getChange().getId());
+    try {
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
+      db.patchSetApprovals().insert(toInsert);
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+
+    CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(rsrc.getChange());
     accountLoaderFactory.create(true).fill(result.reviewers);
     postAdd(rsrc.getChange(), result);
+    indexFuture.checkedGet();
   }
 
   private void postAdd(Change change, PostResult result)
@@ -247,10 +276,10 @@
 
     // Execute hook for added reviewers
     //
-    PatchSet patchSet = db.get().patchSets().get(change.currentPatchSetId());
+    PatchSet patchSet = dbProvider.get().patchSets().get(change.currentPatchSetId());
     for (AccountInfo info : result.reviewers) {
       Account account = accountCache.get(info._id).getAccount();
-      hooks.doReviewerAddedHook(change, account, patchSet, db.get());
+      hooks.doReviewerAddedHook(change, account, patchSet, dbProvider.get());
     }
 
     // Email the reviewers
@@ -264,12 +293,15 @@
       }
     }
     if (!added.isEmpty()) {
-      AddReviewerSender cm;
-
-      cm = addReviewerSenderFactory.create(change);
-      cm.setFrom(currentUser.getAccountId());
-      cm.addReviewers(added);
-      cm.send();
+      try {
+        AddReviewerSender cm = addReviewerSenderFactory.create(change);
+        cm.setFrom(currentUser.getAccountId());
+        cm.addReviewers(added);
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot send email to new reviewers of change "
+            + change.getId(), err);
+      }
     }
   }
 
@@ -283,7 +315,8 @@
     LabelId id =
         Iterables.getLast(ctl.getLabelTypes().getLabelTypes()).getLabelId();
     PatchSetApproval dummyApproval = new PatchSetApproval(
-        new PatchSetApproval.Key(patchSetId, reviewerId, id), (short) 0);
+        new PatchSetApproval.Key(patchSetId, reviewerId, id), (short) 0,
+        TimeUtil.nowTs());
     dummyApproval.cache(ctl.getChange());
     return dummyApproval;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
new file mode 100644
index 0000000..8b0fe99
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
@@ -0,0 +1,164 @@
+// 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.change;
+
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+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.webui.UiAction;
+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.change.Publish.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.mail.PatchSetNotificationSender;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class Publish implements RestModifyView<RevisionResource, Input>,
+    UiAction<RevisionResource> {
+  public static class Input {
+  }
+
+  private final Provider<ReviewDb> dbProvider;
+  private final PatchSetNotificationSender sender;
+  private final ChangeHooks hooks;
+  private final ChangeIndexer indexer;
+
+  @Inject
+  public Publish(Provider<ReviewDb> dbProvider,
+      PatchSetNotificationSender sender,
+      ChangeHooks hooks,
+      ChangeIndexer indexer) {
+    this.dbProvider = dbProvider;
+    this.sender = sender;
+    this.hooks = hooks;
+    this.indexer = indexer;
+  }
+
+  @Override
+  public Object apply(RevisionResource rsrc, Input input) throws IOException,
+      ResourceNotFoundException, ResourceConflictException,
+      OrmException, AuthException {
+    if (!rsrc.getPatchSet().isDraft()) {
+      throw new ResourceConflictException("Patch set is not a draft");
+    }
+
+    if (!rsrc.getControl().canPublish(dbProvider.get())) {
+      throw new AuthException("Cannot publish this draft patch set");
+    }
+
+    PatchSet updatedPatchSet = updateDraftPatchSet(rsrc);
+    Change updatedChange = updateDraftChange(rsrc);
+
+    try {
+      if (!updatedPatchSet.isDraft()
+          || updatedChange.getStatus() == Change.Status.NEW) {
+        CheckedFuture<?, IOException> indexFuture =
+            indexer.indexAsync(updatedChange);
+        hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, dbProvider.get());
+        sender.send(rsrc.getChange().getStatus() == Change.Status.DRAFT,
+            rsrc.getUser(), updatedChange, updatedPatchSet,
+            rsrc.getControl().getLabelTypes());
+        indexFuture.checkedGet();
+      }
+    } catch (PatchSetInfoNotAvailableException e) {
+      throw new ResourceNotFoundException(e.getMessage());
+    }
+
+    return Response.none();
+  }
+
+  private Change updateDraftChange(RevisionResource rsrc) throws OrmException {
+    Change updatedChange = 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;
+      }
+    });
+    return updatedChange;
+  }
+
+  private PatchSet updateDraftPatchSet(RevisionResource rsrc) throws OrmException {
+    final PatchSet updatedPatchSet = dbProvider.get().patchSets()
+        .atomicUpdate(rsrc.getPatchSet().getId(),
+        new AtomicUpdate<PatchSet>() {
+      @Override
+      public PatchSet update(PatchSet patchset) {
+        patchset.setDraft(false);
+        return patchset;
+      }
+    });
+    return updatedPatchSet;
+  }
+
+  @Override
+  public UiAction.Description getDescription(RevisionResource rsrc) {
+    PatchSet.Id current = rsrc.getChange().currentPatchSetId();
+    try {
+      return new UiAction.Description()
+        .setTitle(String.format("Publish revision %d",
+            rsrc.getPatchSet().getPatchSetId()))
+        .setVisible(rsrc.getPatchSet().isDraft()
+            && rsrc.getPatchSet().getId().equals(current)
+            && rsrc.getControl().canPublish(dbProvider.get()));
+    } catch (OrmException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  public static class CurrentRevision implements
+      RestModifyView<ChangeResource, Input> {
+    private final Provider<ReviewDb> dbProvider;
+    private final Publish publish;
+
+    @Inject
+    CurrentRevision(Provider<ReviewDb> dbProvider,
+        Publish publish) {
+      this.dbProvider = dbProvider;
+      this.publish = publish;
+    }
+
+    @Override
+    public Object apply(ChangeResource rsrc, Input input) throws AuthException,
+        ResourceConflictException, ResourceConflictException, IOException,
+        OrmException, ResourceNotFoundException, AuthException {
+      PatchSet ps = dbProvider.get().patchSets()
+        .get(rsrc.getChange().currentPatchSetId());
+      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");
+      }
+      return publish.apply(new RevisionResource(rsrc, ps), input);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
index befb8d7..803af17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -22,9 +23,10 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.CommentRange;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.CommentInfo.Side;
 import com.google.gerrit.server.change.PutDraft.Input;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -41,6 +43,7 @@
     Integer line;
     String inReplyTo;
     Timestamp updated; // Accepted but ignored.
+    CommentRange range;
 
     @DefaultInput
     String message;
@@ -67,6 +70,8 @@
       throw new BadRequestException("id must match URL");
     } else if (in.line != null && in.line < 0) {
       throw new BadRequestException("line must be >= 0");
+    } else if (in.line != null && in.range != null && in.line != in.range.getEndLine()) {
+      throw new BadRequestException("range endLine must be on the same line as the comment");
     }
 
     if (in.path != null
@@ -80,7 +85,7 @@
               c.getKey().get()),
           c.getLine(),
           rsrc.getAuthorId(),
-          c.getParentUuid());
+          c.getParentUuid(), TimeUtil.nowTs());
       db.get().patchComments().insert(Collections.singleton(update(c, in)));
     } else {
       db.get().patchComments().update(Collections.singleton(update(c, in)));
@@ -90,16 +95,17 @@
 
   private PatchLineComment update(PatchLineComment e, Input in) {
     if (in.side != null) {
-      e.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
-    }
-    if (in.line != null) {
-      e.setLine(in.line);
+      e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
     }
     if (in.inReplyTo != null) {
       e.setParentUuid(Url.decode(in.inReplyTo));
     }
     e.setMessage(in.message.trim());
-    e.updated();
+    if (in.range != null || in.line != null) {
+      e.setRange(in.range);
+      e.setLine(in.range != null ? in.range.getEndLine() : in.line);
+    }
+    e.updated(TimeUtil.nowTs());
     return e;
   }
 }
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 b96b480..acf96e6 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
@@ -15,27 +15,36 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
-import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 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.project.ChangeControl;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import java.io.IOException;
 import java.util.Collections;
 
-class PutTopic implements RestModifyView<ChangeResource, Input> {
+class PutTopic implements RestModifyView<ChangeResource, Input>,
+    UiAction<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
+  private final ChangeIndexer indexer;
+  private final ChangeHooks hooks;
 
   static class Input {
     @DefaultInput
@@ -44,8 +53,11 @@
   }
 
   @Inject
-  PutTopic(Provider<ReviewDb> dbProvider) {
+  PutTopic(Provider<ReviewDb> dbProvider, ChangeIndexer indexer,
+      ChangeHooks hooks) {
     this.dbProvider = dbProvider;
+    this.indexer = indexer;
+    this.hooks = hooks;
   }
 
   @Override
@@ -77,9 +89,10 @@
             oldTopicName, newTopicName);
       }
 
+      IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
       ChangeMessage cmsg = new ChangeMessage(
           new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
-          ((IdentifiedUser) control.getCurrentUser()).getAccountId(),
+          currentUser.getAccountId(), TimeUtil.nowTs(),
           change.currentPatchSetId());
       StringBuilder msgBuf = new StringBuilder(summary);
       if (!Strings.isNullOrEmpty(input.message)) {
@@ -88,18 +101,36 @@
       }
       cmsg.setMessage(msgBuf.toString());
 
-      db.changes().atomicUpdate(change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            change.setTopic(Strings.emptyToNull(newTopicName));
-            return change;
-          }
-        });
-      db.changeMessages().insert(Collections.singleton(cmsg));
+      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;
+            }
+          });
+        db.changeMessages().insert(Collections.singleton(cmsg));
+        db.commit();
+      } finally {
+        db.rollback();
+      }
+      CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
+      hooks.doTopicChangedHook(change, currentUser.getAccount(),
+          oldTopicName, db);
+      indexFuture.checkedGet();
     }
     return Strings.isNullOrEmpty(newTopicName)
         ? Response.none()
         : newTopicName;
   }
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource resource) {
+    return new UiAction.Description()
+      .setLabel("Edit Topic")
+      .setVisible(resource.getControl().canEditTopicName());
+  }
 }
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
new file mode 100644
index 0000000..3831c20
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -0,0 +1,117 @@
+// 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.change;
+
+import com.google.gerrit.common.changes.ListChangesOption;
+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.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.Rebase.Input;
+import com.google.gerrit.server.changedetail.RebaseChange;
+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 java.io.IOException;
+
+public class Rebase implements RestModifyView<RevisionResource, Input>,
+    UiAction<RevisionResource> {
+  public static class Input {
+  }
+
+  private final Provider<RebaseChange> rebaseChange;
+  private final ChangeJson json;
+
+  @Inject
+  public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json) {
+    this.rebaseChange = rebaseChange;
+    this.json = json;
+  }
+
+  @Override
+  public ChangeInfo apply(RevisionResource rsrc, Input input)
+      throws AuthException, ResourceNotFoundException,
+      ResourceConflictException, EmailException, OrmException {
+    ChangeControl control = rsrc.getControl();
+    Change change = rsrc.getChange();
+    if (!control.canRebase()) {
+      throw new AuthException("rebase not permitted");
+    } else if (!change.getStatus().isOpen()) {
+      throw new ResourceConflictException("change is "
+          + change.getStatus().name().toLowerCase());
+    }
+
+    try {
+      rebaseChange.get().rebase(rsrc.getPatchSet().getId(), rsrc.getUser());
+    } catch (InvalidChangeOperationException e) {
+      throw new ResourceConflictException(e.getMessage());
+    } catch (IOException e) {
+      throw new ResourceConflictException(e.getMessage());
+    } catch (NoSuchChangeException e) {
+      throw new ResourceNotFoundException(change.getId().toString());
+    }
+
+    json.addOption(ListChangesOption.CURRENT_REVISION)
+        .addOption(ListChangesOption.CURRENT_COMMIT);
+    return json.format(change.getId());
+  }
+
+  @Override
+  public UiAction.Description getDescription(RevisionResource resource) {
+    return new UiAction.Description()
+      .setLabel("Rebase")
+      .setTitle("Rebase onto tip of branch or parent change")
+      .setVisible(resource.getChange().getStatus().isOpen()
+          && resource.getControl().canRebase()
+          && rebaseChange.get().canRebase(resource));
+  }
+
+  public static class CurrentRevision implements
+      RestModifyView<ChangeResource, Input> {
+    private final Provider<ReviewDb> dbProvider;
+    private final Rebase rebase;
+
+    @Inject
+    CurrentRevision(Provider<ReviewDb> dbProvider, Rebase rebase) {
+      this.dbProvider = dbProvider;
+      this.rebase = rebase;
+    }
+
+    @Override
+    public ChangeInfo apply(ChangeResource rsrc, Input input)
+        throws AuthException, ResourceNotFoundException,
+        ResourceConflictException, EmailException, OrmException {
+      PatchSet ps =
+          dbProvider.get().patchSets()
+              .get(rsrc.getChange().currentPatchSetId());
+      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");
+      }
+      return rebase.apply(new RevisionResource(rsrc, ps), input);
+    }
+  }
+}
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 afb58f9..d31d1f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -27,7 +29,9 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.change.Restore.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.project.ChangeControl;
@@ -39,15 +43,18 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Collections;
 
-public class Restore implements RestModifyView<ChangeResource, Input> {
+public class Restore implements RestModifyView<ChangeResource, Input>,
+    UiAction<ChangeResource> {
   private static final Logger log = LoggerFactory.getLogger(Restore.class);
 
   private final ChangeHooks hooks;
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
+  private final ChangeIndexer indexer;
 
   public static class Input {
     @DefaultInput
@@ -58,11 +65,13 @@
   Restore(ChangeHooks hooks,
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json) {
+      ChangeJson json,
+      ChangeIndexer indexer) {
     this.hooks = hooks;
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
+    this.indexer = indexer;
   }
 
   @Override
@@ -86,8 +95,8 @@
         new AtomicUpdate<Change>() {
           @Override
           public Change update(Change change) {
-            if (change.getStatus() == Change.Status.ABANDONED) {
-              change.setStatus(Change.Status.NEW);
+            if (change.getStatus() == Status.ABANDONED) {
+              change.setStatus(Status.NEW);
               ChangeUtil.updated(change);
               return change;
             }
@@ -106,6 +115,7 @@
       db.rollback();
     }
 
+    CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
     try {
       ReplyToChangeSender cm = restoredSenderFactory.create(change);
       cm.setFrom(caller.getAccountId());
@@ -116,9 +126,21 @@
     }
     hooks.doChangeRestoredHook(change,
         caller.getAccount(),
+        db.patchSets().get(change.currentPatchSetId()),
         Strings.emptyToNull(input.message),
         dbProvider.get());
-    return json.format(change);
+    ChangeInfo result = json.format(change);
+    indexFuture.checkedGet();
+    return result;
+  }
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource resource) {
+    return new UiAction.Description()
+      .setLabel("Restore")
+      .setTitle("Restore the change")
+      .setVisible(resource.getChange().getStatus() == Status.ABANDONED
+          && resource.getControl().canRestore());
   }
 
   private ChangeMessage newMessage(Input input, IdentifiedUser caller,
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 154bd64..73a6b03 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -40,7 +41,8 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Revert implements RestModifyView<ChangeResource, Input> {
+public class Revert implements RestModifyView<ChangeResource, Input>,
+    UiAction<ChangeResource> {
   private final ChangeHooks hooks;
   private final RevertedSender.Factory revertedSenderFactory;
   private final CommitValidators.Factory commitValidatorsFactory;
@@ -49,7 +51,7 @@
   private final GitRepositoryManager gitManager;
   private final PersonIdent myIdent;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ChangeInserter changeInserter;
+  private final ChangeInserter.Factory changeInserterFactory;
 
   public static class Input {
     public String message;
@@ -64,7 +66,7 @@
       GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
       @GerritPersonIdent final PersonIdent myIdent,
-      final ChangeInserter changeInserter) {
+      final ChangeInserter.Factory changeInserterFactory) {
     this.hooks = hooks;
     this.revertedSenderFactory = revertedSenderFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
@@ -72,7 +74,7 @@
     this.json = json;
     this.gitManager = gitManager;
     this.myIdent = myIdent;
-    this.changeInserter = changeInserter;
+    this.changeInserterFactory = changeInserterFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
   }
 
@@ -97,7 +99,7 @@
               commitValidators,
               Strings.emptyToNull(input.message), dbProvider.get(),
               revertedSenderFactory, hooks, git, patchSetInfoFactory,
-              myIdent, changeInserter);
+              myIdent, changeInserterFactory);
 
       return json.format(revertedChangeId);
     } catch (InvalidChangeOperationException e) {
@@ -107,6 +109,15 @@
     }
   }
 
+  @Override
+  public UiAction.Description getDescription(ChangeResource resource) {
+    return new UiAction.Description()
+      .setLabel("Revert")
+      .setTitle("Revert the change")
+      .setVisible(resource.getChange().getStatus() == Status.MERGED
+          && resource.getControl().getRefControl().canUpload());
+  }
+
   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/Reviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
index 0898bce..022f178 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountPatchReview;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -28,7 +29,7 @@
   static class Input {
   }
 
-  static class PutReviewed implements RestModifyView<PatchResource, Input> {
+  static class PutReviewed implements RestModifyView<FileResource, Input> {
     private final Provider<ReviewDb> dbProvider;
 
     @Inject
@@ -37,14 +38,18 @@
     }
 
     @Override
-    public Object apply(PatchResource resource, Input input)
+    public Object apply(FileResource resource, Input input)
         throws OrmException {
       ReviewDb db = dbProvider.get();
       AccountPatchReview apr = getExisting(db, resource);
       if (apr == null) {
-        db.accountPatchReviews().insert(
-            Collections.singleton(new AccountPatchReview(resource.getPatchKey(),
-                resource.getAccountId())));
+        try {
+          db.accountPatchReviews().insert(
+              Collections.singleton(new AccountPatchReview(resource.getPatchKey(),
+                  resource.getAccountId())));
+        } catch (OrmDuplicateKeyException e) {
+          return Response.ok("");
+        }
         return Response.created("");
       } else {
         return Response.ok("");
@@ -52,7 +57,7 @@
     }
   }
 
-  static class DeleteReviewed implements RestModifyView<PatchResource, Input> {
+  static class DeleteReviewed implements RestModifyView<FileResource, Input> {
     private final Provider<ReviewDb> dbProvider;
 
     @Inject
@@ -61,7 +66,7 @@
     }
 
     @Override
-    public Object apply(PatchResource resource, Input input)
+    public Object apply(FileResource resource, Input input)
         throws OrmException {
       ReviewDb db = dbProvider.get();
       AccountPatchReview apr = getExisting(db, resource);
@@ -73,7 +78,7 @@
   }
 
   private static AccountPatchReview getExisting(ReviewDb db,
-      PatchResource resource) throws OrmException {
+      FileResource resource) throws OrmException {
     AccountPatchReview.Key key = new AccountPatchReview.Key(
         resource.getPatchKey(), resource.getAccountId());
     return db.accountPatchReviews().get(key);
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 cdd9e0f..b15719f 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 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.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -23,20 +24,29 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 
-public class RevisionResource implements RestResource {
+public class RevisionResource implements RestResource, HasETag {
   public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND =
       new TypeLiteral<RestView<RevisionResource>>() {};
 
   private final ChangeResource change;
   private final PatchSet ps;
+  private boolean cacheable = true;
 
   public RevisionResource(ChangeResource change, PatchSet ps) {
     this.change = change;
     this.ps = ps;
   }
 
+  public boolean isCacheable() {
+    return cacheable;
+  }
+
+  public ChangeResource getChangeResource() {
+    return change;
+  }
+
   public ChangeControl getControl() {
-    return change.getControl();
+    return getChangeResource().getControl();
   }
 
   public Change getChange() {
@@ -47,7 +57,24 @@
     return ps;
   }
 
+  @Override
+  public String getETag() {
+    // Conservative estimate: refresh the revision if its parent change has
+    // changed, so we don't have to check whether a given modification affected
+    // this revision specifically.
+    return change.getETag();
+  }
+
   Account.Id getAccountId() {
-    return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
+    return getUser().getAccountId();
+  }
+
+  IdentifiedUser getUser() {
+    return (IdentifiedUser) getControl().getCurrentUser();
+  }
+
+  RevisionResource doNotCache() {
+    cacheable = false;
+    return this;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index 68ef63c..19c6d3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -59,7 +59,7 @@
       PatchSet.Id p = change.getChange().currentPatchSetId();
       PatchSet ps = p != null ? dbProvider.get().patchSets().get(p) : null;
       if (ps != null && visible(change, ps)) {
-        return new RevisionResource(change, ps);
+        return new RevisionResource(change, ps).doNotCache();
       }
       throw new ResourceNotFoundException(id);
     }
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 78202fc..3389031 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -36,7 +37,9 @@
 import com.google.gerrit.server.change.Submit.Input;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -50,7 +53,8 @@
 import java.util.Collections;
 import java.util.List;
 
-public class Submit implements RestModifyView<RevisionResource, Input> {
+public class Submit implements RestModifyView<RevisionResource, Input>,
+    UiAction<RevisionResource> {
   public static class Input {
     public boolean waitForMerge;
   }
@@ -72,14 +76,17 @@
   private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
   private final MergeQueue mergeQueue;
+  private final ChangeIndexer indexer;
 
   @Inject
   Submit(Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
-      MergeQueue mergeQueue) {
+      MergeQueue mergeQueue,
+      ChangeIndexer indexer) {
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.mergeQueue = mergeQueue;
+    this.indexer = indexer;
   }
 
   @Override
@@ -136,6 +143,18 @@
     }
   }
 
+  @Override
+  public UiAction.Description getDescription(RevisionResource resource) {
+    PatchSet.Id current = resource.getChange().currentPatchSetId();
+    return new UiAction.Description()
+      .setTitle(String.format(
+          "Submit revision %d",
+          resource.getPatchSet().getPatchSetId()))
+      .setVisible(resource.getChange().getStatus().isOpen()
+          && resource.getPatchSet().getId().equals(current)
+          && resource.getControl().canSubmit());
+  }
+
   /**
    * If the merge was attempted and it failed the system usually writes a
    * comment as a ChangeMessage and sets status to NEW. Find the relevant
@@ -157,8 +176,8 @@
   }
 
   public Change submit(RevisionResource rsrc, IdentifiedUser caller)
-      throws OrmException {
-    final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+      throws OrmException, IOException {
+    final Timestamp timestamp = TimeUtil.nowTs();
     Change change = rsrc.getChange();
     ReviewDb db = dbProvider.get();
     db.changes().beginTransaction(change.getId());
@@ -185,6 +204,7 @@
     } finally {
       db.rollback();
     }
+    indexer.index(change);
     return change;
   }
 
@@ -205,7 +225,7 @@
               rev.getId(),
               caller.getAccountId(),
               LabelId.SUBMIT),
-          (short) 1);
+          (short) 1, TimeUtil.nowTs());
     }
     submit.setValue((short) 1);
     submit.setGranted(timestamp);
@@ -290,7 +310,7 @@
     });
   }
 
-  private static String status(Change change) {
+  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/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
new file mode 100644
index 0000000..1a670cc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -0,0 +1,296 @@
+// 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.change;
+
+import com.google.common.base.Objects;
+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.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupJson.GroupBaseInfo;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class SuggestReviewers implements RestReadView<ChangeResource> {
+
+  private static final String MAX_SUFFIX = "\u9fa5";
+  private static final int MAX = 10;
+
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountControl.Factory accountControlFactory;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final AccountCache accountCache;
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<CurrentUser> currentUser;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final GroupBackend groupBackend;
+  private final boolean suggestAccounts;
+  private final int suggestFrom;
+  private final int maxAllowed;
+  private int limit;
+  private String query;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
+      usage = "maximum number of reviewers to list")
+  public void setLimit(int l) {
+    this.limit = l <= 0 ? MAX : Math.min(l, MAX);
+  }
+
+  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY",
+      usage = "match reviewers query")
+  public void setQuery(String q) {
+    this.query = q;
+  }
+
+  @Inject
+  SuggestReviewers(AccountVisibility av,
+      AccountInfo.Loader.Factory accountLoaderFactory,
+      AccountControl.Factory accountControlFactory,
+      AccountCache accountCache,
+      GroupMembers.Factory groupMembersFactory,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      Provider<CurrentUser> currentUser,
+      Provider<ReviewDb> dbProvider,
+      @GerritServerConfig Config cfg,
+      GroupBackend groupBackend) {
+    this.accountLoaderFactory = accountLoaderFactory;
+    this.accountControlFactory = accountControlFactory;
+    this.accountCache = accountCache;
+    this.groupMembersFactory = groupMembersFactory;
+    this.dbProvider = dbProvider;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.currentUser = currentUser;
+    this.groupBackend = groupBackend;
+
+    String suggest = cfg.getString("suggest", null, "accounts");
+    if ("OFF".equalsIgnoreCase(suggest)
+        || "false".equalsIgnoreCase(suggest)) {
+      this.suggestAccounts = false;
+    } else {
+      this.suggestAccounts = (av != AccountVisibility.NONE);
+    }
+
+    this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
+    this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
+        PostReviewers.DEFAULT_MAX_REVIEWERS);
+  }
+
+  private interface VisibilityControl {
+    boolean isVisibleTo(Account account) throws OrmException;
+  }
+
+  @Override
+  public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
+      throws BadRequestException, OrmException, IOException {
+    if (Strings.isNullOrEmpty(query)) {
+      throw new BadRequestException("missing query field");
+    }
+
+    if (!suggestAccounts || query.length() < suggestFrom) {
+      return Collections.emptyList();
+    }
+
+    VisibilityControl visibilityControl = getVisibility(rsrc);
+    List<AccountInfo> suggestedAccounts = suggestAccount(visibilityControl);
+    accountLoaderFactory.create(true).fill(suggestedAccounts);
+
+    List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
+    for (AccountInfo a : suggestedAccounts) {
+      reviewer.add(new SuggestedReviewerInfo(a));
+    }
+
+    Project p = rsrc.getControl().getProject();
+    for (GroupReference g : suggestAccountGroup(
+        rsrc.getControl().getProjectControl())) {
+      if (suggestGroupAsReviewer(p, g, visibilityControl)) {
+        GroupBaseInfo info = new GroupBaseInfo();
+        info.id = Url.encode(g.getUUID().get());
+        info.name = g.getName();
+        reviewer.add(new SuggestedReviewerInfo(info));
+      }
+    }
+
+    Collections.sort(reviewer);
+    if (reviewer.size() <= limit) {
+      return reviewer;
+    } else {
+      return reviewer.subList(0, limit);
+    }
+  }
+
+  private VisibilityControl getVisibility(final ChangeResource rsrc) {
+    if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account account) throws OrmException {
+          return true;
+        }
+      };
+    } else {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account account) throws OrmException {
+          IdentifiedUser who =
+              identifiedUserFactory.create(dbProvider, account.getId());
+          // we can't use changeControl directly as it won't suggest reviewers
+          // to drafts
+          return rsrc.getControl().forUser(who).isRefVisible();
+        }
+      };
+    }
+  }
+
+  private List<GroupReference> suggestAccountGroup(ProjectControl ctl) {
+    return Lists.newArrayList(
+        Iterables.limit(groupBackend.suggest(query, ctl), limit));
+  }
+
+  private List<AccountInfo> suggestAccount(VisibilityControl visibilityControl)
+      throws OrmException {
+    String a = query;
+    String b = a + MAX_SUFFIX;
+
+    LinkedHashMap<Account.Id, AccountInfo> r = Maps.newLinkedHashMap();
+    for (Account p : dbProvider.get().accounts()
+        .suggestByFullName(a, b, limit)) {
+      addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
+    }
+
+    if (r.size() < limit) {
+      for (Account p : dbProvider.get().accounts()
+          .suggestByPreferredEmail(a, b, limit - r.size())) {
+        addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
+      }
+    }
+
+    if (r.size() < limit) {
+      for (AccountExternalId e : dbProvider.get().accountExternalIds()
+          .suggestByEmailAddress(a, b, limit - r.size())) {
+        if (!r.containsKey(e.getAccountId())) {
+          Account p = accountCache.get(e.getAccountId()).getAccount();
+          AccountInfo info = new AccountInfo(p.getId());
+          addSuggestion(r, p, info, visibilityControl);
+        }
+      }
+    }
+
+    return Lists.newArrayList(r.values());
+  }
+
+  private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
+      AccountInfo info, VisibilityControl visibilityControl)
+      throws OrmException {
+    if (!map.containsKey(account.getId())
+        && account.isActive()
+        // Can the suggestion see the change?
+        && visibilityControl.isVisibleTo(account)
+        // Can the account see the current user?
+        && accountControlFactory.get().canSee(account)) {
+      map.put(account.getId(), info);
+    }
+  }
+
+  private boolean suggestGroupAsReviewer(Project project,
+      GroupReference group, VisibilityControl visibilityControl)
+      throws OrmException, IOException {
+    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
+      return false;
+    }
+
+    try {
+      Set<Account> members = groupMembersFactory
+          .create(currentUser.get())
+          .listAccounts(group.getUUID(), project.getNameKey());
+
+      if (members.isEmpty()) {
+        return false;
+      }
+
+      if (maxAllowed > 0 && members.size() > maxAllowed) {
+        return false;
+      }
+
+      // require that at least one member in the group can see the change
+      for (Account account : members) {
+        if (visibilityControl.isVisibleTo(account)) {
+          return true;
+        }
+      }
+    } catch (NoSuchGroupException e) {
+      return false;
+    } catch (NoSuchProjectException e) {
+      return false;
+    }
+
+    return false;
+  }
+
+  static class SuggestedReviewerInfo implements Comparable<SuggestedReviewerInfo> {
+    String kind = "gerritcodereview#suggestedreviewer";
+    AccountInfo account;
+    GroupBaseInfo group;
+
+    SuggestedReviewerInfo(AccountInfo a) {
+      this.account = a;
+    }
+
+    SuggestedReviewerInfo(GroupBaseInfo g) {
+      this.group = g;
+    }
+
+    @Override
+    public int compareTo(SuggestedReviewerInfo o) {
+      return getSortValue().compareTo(o.getSortValue());
+    }
+
+    private String getSortValue() {
+      return account != null
+          ? Objects.firstNonNull(account.email,
+              Strings.nullToEmpty(account.name))
+          : Strings.nullToEmpty(group.name);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
index e7e1f32..5a4f9e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.change.TestSubmitRule.Filters;
@@ -30,6 +31,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
 import org.kohsuke.args4j.Option;
@@ -51,8 +53,8 @@
   }
 
   @Override
-  public String apply(RevisionResource rsrc, Input input) throws OrmException,
-      BadRequestException, AuthException {
+  public SubmitType apply(RevisionResource rsrc, Input input)
+      throws OrmException, BadRequestException, AuthException {
     if (input == null) {
       input = new Input();
     }
@@ -96,7 +98,16 @@
           evaluator.getSubmitRule().toString(),
           type));
     }
-    return type.toString();
+
+    String typeName = ((SymbolTerm) type).name();
+    try {
+      return SubmitType.valueOf(typeName.toUpperCase());
+    } catch (IllegalArgumentException e) {
+      throw new BadRequestException(String.format(
+          "rule %s produced invalid result: %s",
+          evaluator.getSubmitRule().toString(),
+          type));
+    }
   }
 
   static class Get implements RestReadView<RevisionResource> {
@@ -108,8 +119,8 @@
     }
 
     @Override
-    public String apply(RevisionResource resource) throws BadRequestException,
-        OrmException, AuthException {
+    public SubmitType apply(RevisionResource resource)
+        throws BadRequestException, OrmException, AuthException {
       return test.apply(resource, null);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
index d20b0f6..e6ce4c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -46,6 +47,7 @@
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ChangeIndexer indexer;
 
   private final PatchSet.Id patchSetId;
 
@@ -53,12 +55,14 @@
   DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
       ReviewDb db, GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated, PatchSetInfoFactory patchSetInfoFactory,
+      ChangeIndexer indexer,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
     this.patchSetInfoFactory = patchSetInfoFactory;
+    this.indexer = indexer;
 
     this.patchSetId = patchSetId;
   }
@@ -97,7 +101,8 @@
     List<PatchSet> restOfPatches = db.patchSets().byChange(changeId).toList();
     if (restOfPatches.size() == 0) {
       try {
-        ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db);
+        ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db,
+            indexer);
         result.setChangeId(null);
       } catch (IOException e) {
         result.addError(new ReviewResult.Error(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
index 22eae2d..82b74d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -15,26 +15,21 @@
 
 package com.google.gerrit.server.changedetail;
 
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
+import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.ReviewResult;
-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.PatchSetApproval;
-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.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
-import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.PatchSetNotificationSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
@@ -45,23 +40,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-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;
 import java.util.concurrent.Callable;
 
 public class PublishDraft implements Callable<ReviewResult> {
-  private static final Logger log =
-      LoggerFactory.getLogger(PublishDraft.class);
-
   public interface Factory {
     PublishDraft create(PatchSet.Id patchSetId);
   }
@@ -69,12 +51,8 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final ChangeHooks hooks;
-  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;
+  private final ChangeIndexer indexer;
+  private final PatchSetNotificationSender sender;
 
   private final PatchSet.Id patchSetId;
 
@@ -87,16 +65,14 @@
       final AccountResolver accountResolver,
       final CreateChangeSender.Factory createChangeSenderFactory,
       final ReplacePatchSetSender.Factory replacePatchSetFactory,
+      final ChangeIndexer indexer,
+      final PatchSetNotificationSender sender,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.hooks = hooks;
-    this.repoManager = repoManager;
-    this.patchSetInfoFactory = patchSetInfoFactory;
-    this.approvalsUtil = approvalsUtil;
-    this.accountResolver = accountResolver;
-    this.createChangeSenderFactory = createChangeSenderFactory;
-    this.replacePatchSetFactory = replacePatchSetFactory;
+    this.indexer = indexer;
+    this.sender = sender;
 
     this.patchSetId = patchSetId;
   }
@@ -146,76 +122,16 @@
       });
 
       if (!updatedPatchSet.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
+        CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(updatedChange);
         hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, db);
 
-        sendNotifications(control.getChange().getStatus() == Change.Status.DRAFT,
+        sender.send(control.getChange().getStatus() == Change.Status.DRAFT,
             (IdentifiedUser) control.getCurrentUser(), updatedChange, updatedPatchSet,
             labelTypes);
+        indexFuture.checkedGet();
       }
     }
 
     return result;
   }
-
-  private void sendNotifications(final boolean newChange,
-      final IdentifiedUser currentUser, final Change updatedChange,
-      final PatchSet updatedPatchSet, final LabelTypes labelTypes)
-      throws OrmException, IOException, PatchSetInfoNotAvailableException {
-    final Repository git = repoManager.openRepository(updatedChange.getProject());
-    try {
-      final RevWalk revWalk = new RevWalk(git);
-      final RevCommit commit;
-      try {
-        commit = revWalk.parseCommit(ObjectId.fromString(updatedPatchSet.getRevision().get()));
-      } finally {
-        revWalk.release();
-      }
-      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, labelTypes, updatedChange, updatedPatchSet, info,
-            recipients.getReviewers(), Collections.<Account.Id> emptySet());
-        try {
-          CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
-          cm.setFrom(me);
-          cm.setPatchSet(updatedPatchSet, info);
-          cm.addReviewers(recipients.getReviewers());
-          cm.addExtraCC(recipients.getCcOnly());
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new change " + updatedChange.getId(), e);
-        }
-      } else {
-        final List<PatchSetApproval> patchSetApprovals =
-            db.patchSetApprovals().byChange(updatedChange.getId()).toList();
-        final MailRecipients oldRecipients =
-            getRecipientsFromApprovals(patchSetApprovals);
-        approvalsUtil.addReviewers(db, labelTypes, updatedChange, updatedPatchSet, info,
-            recipients.getReviewers(), oldRecipients.getAll());
-        final ChangeMessage msg =
-            new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
-                ChangeUtil.messageUUID(db)), me,
-                updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
-        msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
-        try {
-          ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
-          cm.setFrom(me);
-          cm.setPatchSet(updatedPatchSet, info);
-          cm.setChangeMessage(msg);
-          cm.addReviewers(recipients.getReviewers());
-          cm.addExtraCC(recipients.getCcOnly());
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
-        }
-      }
-    } finally {
-      git.close();
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index d7bf5a3..b766cea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -14,34 +14,28 @@
 
 package com.google.gerrit.server.changedetail;
 
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.ChangeHookRunner;
+import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
+
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.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.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.mail.RebasedPatchSetSender;
-import com.google.gerrit.server.mail.ReplacePatchSetSender;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
 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.ProjectCache;
-import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -51,50 +45,35 @@
 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.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 public class RebaseChange {
-  private final ChangeControl.Factory changeControlFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
   private final PersonIdent myIdent;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
-  private final ChangeHookRunner hooks;
   private final MergeUtil.Factory mergeUtilFactory;
-  private final ProjectCache projectCache;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
 
   @Inject
-  RebaseChange(final ChangeControl.Factory changeControlFactory,
-      final PatchSetInfoFactory patchSetInfoFactory, final ReviewDb db,
+  RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
+      final ReviewDb db,
       @GerritPersonIdent final PersonIdent myIdent,
       final GitRepositoryManager gitManager,
-      final GitReferenceUpdated gitRefUpdated,
-      final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
-      final ChangeHookRunner hooks,
       final MergeUtil.Factory mergeUtilFactory,
-      final ProjectCache projectCache) {
+      final PatchSetInserter.Factory patchSetInserterFactory) {
     this.changeControlFactory = changeControlFactory;
-    this.patchSetInfoFactory = patchSetInfoFactory;
     this.db = db;
     this.gitManager = gitManager;
     this.myIdent = myIdent;
-    this.gitRefUpdated = gitRefUpdated;
-    this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
-    this.hooks = hooks;
     this.mergeUtilFactory = mergeUtilFactory;
-    this.projectCache = projectCache;
+    this.patchSetInserterFactory = patchSetInserterFactory;
   }
 
   /**
@@ -123,12 +102,12 @@
    * @throws IOException thrown if rebase is not possible or not needed
    * @throws InvalidChangeOperationException thrown if rebase is not allowed
    */
-  public void rebase(final PatchSet.Id patchSetId, final Account.Id uploader)
+  public void rebase(final PatchSet.Id patchSetId, final IdentifiedUser uploader)
       throws NoSuchChangeException, EmailException, OrmException, IOException,
       InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl changeControl =
-        changeControlFactory.validateFor(changeId);
+        changeControlFactory.validateFor(changeId, uploader);
     if (!changeControl.canRebase()) {
       throw new InvalidChangeOperationException(
           "Cannot rebase: New patch sets are not allowed to be added to change: "
@@ -143,37 +122,19 @@
       rw = new RevWalk(git);
       inserter = git.newObjectInserter();
 
-      final List<PatchSetApproval> oldPatchSetApprovals =
-          db.patchSetApprovals().byChange(change.getId()).toList();
-
       final String baseRev = findBaseRevision(patchSetId, db,
           change.getDest(), git, null, null, null);
       final RevCommit baseCommit =
           rw.parseCommit(ObjectId.fromString(baseRev));
 
-      final PatchSet newPatchSet =
-          rebase(git, rw, inserter, patchSetId, change, uploader, baseCommit,
-              mergeUtilFactory.create(
-                  changeControl.getProjectControl().getProjectState(), true));
+      PersonIdent committerIdent =
+          uploader.newCommitterIdent(myIdent.getWhen(),
+              myIdent.getTimeZone());
 
-      final Set<Account.Id> oldReviewers = Sets.newHashSet();
-      final Set<Account.Id> oldCC = Sets.newHashSet();
-      for (PatchSetApproval a : oldPatchSetApprovals) {
-        if (a.getValue() != 0) {
-          oldReviewers.add(a.getAccountId());
-        } else {
-          oldCC.add(a.getAccountId());
-        }
-      }
-      final ReplacePatchSetSender cm =
-          rebasedPatchSetSenderFactory.create(change);
-      cm.setFrom(uploader);
-      cm.setPatchSet(newPatchSet);
-      cm.addReviewers(oldReviewers);
-      cm.addExtraCC(oldCC);
-      cm.send();
-
-      hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+      rebase(git, rw, inserter, patchSetId, change,
+          uploader, baseCommit, mergeUtilFactory.create(
+              changeControl.getProjectControl().getProjectState(), true),
+          committerIdent, true, true, ValidatePolicy.GERRIT);
     } catch (PathConflictException e) {
       throw new IOException(e.getMessage());
     } finally {
@@ -235,8 +196,7 @@
         depPatchSetList = db.patchSets().byRevision(ancestorRev).toList();
       }
 
-      if (!depPatchSetList.isEmpty()) {
-        PatchSet depPatchSet = depPatchSetList.get(0);
+      for (PatchSet depPatchSet : depPatchSetList) {
 
         Change.Id depChangeId = depPatchSet.getId().getParentKey();
         Change depChange;
@@ -246,6 +206,9 @@
         } else {
           depChange = depChangeList.get(0);
         }
+        if (!depChange.getDest().equals(destBranch)) {
+          continue;
+        }
 
         if (depChange.getStatus() == Status.ABANDONED) {
           throw new IOException("Cannot rebase a change with an abandoned parent: "
@@ -261,6 +224,7 @@
               db.patchSets().get(depChange.currentPatchSetId());
           baseRev = latestDepPatchSet.getRevision().get();
         }
+        break;
       }
 
       if (baseRev == null) {
@@ -285,17 +249,21 @@
    *
    * The rebased commit is added as new patch set to the change.
    *
-   * E-mail notification and triggering of hooks is NOT done for the creation of
-   * the new patch set.
+   * E-mail notification and triggering of hooks is only done for the creation of
+   * the new patch set if `sendEmail` and `runHooks` are set to true.
    *
    * @param git the repository
    * @param revWalk the RevWalk
    * @param inserter the object inserter
    * @param patchSetId the id of the patch set
-   * @param chg the change that should be rebased
+   * @param change the change that should be rebased
    * @param uploader the user that creates the rebased patch set
    * @param baseCommit the commit that should be the new base
    * @param mergeUtil merge utilities for the destination project
+   * @param committerIdent the committer's identity
+   * @param sendMail if a mail notification should be sent for the new patch set
+   * @param runHooks if hooks should be run for the new patch set
+   * @param validate if commit validation should be run for the new patch set
    * @return the new patch set which is based on the given base commit
    * @throws NoSuchChangeException thrown if the change to which the patch set
    *         belongs does not exist or is not visible to the user
@@ -305,12 +273,13 @@
    */
   public PatchSet rebase(final Repository git, final RevWalk revWalk,
       final ObjectInserter inserter, final PatchSet.Id patchSetId,
-      final Change chg, final Account.Id uploader, final RevCommit baseCommit,
-      final MergeUtil mergeUtil) throws NoSuchChangeException,
+      final Change change, final IdentifiedUser uploader, final RevCommit baseCommit,
+      final MergeUtil mergeUtil, PersonIdent committerIdent,
+      boolean sendMail, boolean runHooks, ValidatePolicy validate)
+          throws NoSuchChangeException,
       OrmException, IOException, InvalidChangeOperationException,
       PathConflictException {
-    Change change = chg;
-    if (!chg.currentPatchSetId().equals(patchSetId)) {
+    if (!change.currentPatchSetId().equals(patchSetId)) {
       throw new InvalidChangeOperationException("patch set is not current");
     }
     final PatchSet originalPatchSet = db.patchSets().get(patchSetId);
@@ -318,84 +287,34 @@
     final RevCommit rebasedCommit;
     ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
     ObjectId newId = rebaseCommit(git, inserter, revWalk.parseCommit(oldId),
-        baseCommit, mergeUtil, myIdent);
+        baseCommit, mergeUtil, committerIdent);
 
     rebasedCommit = revWalk.parseCommit(newId);
 
-    PatchSet.Id id = ChangeUtil.nextPatchSetId(git, change.currentPatchSetId());
-    final PatchSet newPatchSet = new PatchSet(id);
-    newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
-    newPatchSet.setUploader(uploader);
-    newPatchSet.setRevision(new RevId(rebasedCommit.name()));
-    newPatchSet.setDraft(originalPatchSet.isDraft());
+    final ChangeControl changeControl =
+        changeControlFactory.validateFor(change.getId(), uploader);
 
-    final PatchSetInfo info =
-        patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId());
+    PatchSetInserter patchSetInserter = patchSetInserterFactory
+        .create(git, revWalk, changeControl.getRefControl(), uploader, change, rebasedCommit)
+        .setCopyLabels(true)
+        .setValidatePolicy(validate)
+        .setDraft(originalPatchSet.isDraft())
+        .setSendMail(sendMail)
+        .setRunHooks(runHooks);
 
-    final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(rebasedCommit);
-    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()));
-    }
-    gitRefUpdated.fire(change.getProject(), ru);
+    final PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
+    final ChangeMessage cmsg = new ChangeMessage(
+        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
+        uploader.getAccountId(), TimeUtil.nowTs(), patchSetId);
 
-    db.changes().beginTransaction(change.getId());
-    try {
-      Change updatedChange = db.changes().get(change.getId());
-      if (updatedChange != null && change.getStatus().isOpen()) {
-        change = updatedChange;
-      } else {
-        throw new InvalidChangeOperationException(String.format(
-            "Change %s is closed", change.getId()));
-      }
+    cmsg.setMessage("Patch Set " + newPatchSetId.get()
+        + ": Patch Set " + patchSetId.get() + " was rebased");
 
-      ChangeUtil.insertAncestors(db, newPatchSet.getId(), rebasedCommit);
-      db.patchSets().insert(Collections.singleton(newPatchSet));
-      updatedChange =
-          db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-            @Override
-            public Change update(Change change) {
-              if (change.getStatus().isClosed()) {
-                return null;
-              }
-              if (!change.currentPatchSetId().equals(patchSetId)) {
-                return null;
-              }
-              if (change.getStatus() != Change.Status.DRAFT) {
-                change.setStatus(Change.Status.NEW);
-              }
-              change.setLastSha1MergeTested(null);
-              change.setCurrentPatchSet(info);
-              ChangeUtil.updated(change);
-              return change;
-            }
-          });
-      if (updatedChange != null) {
-        change = updatedChange;
-      } else {
-        throw new InvalidChangeOperationException(String.format(
-            "Change %s was modified", change.getId()));
-      }
+    Change newChange = patchSetInserter
+        .setMessage(cmsg)
+        .insert();
 
-      ApprovalsUtil.copyLabels(db, projectCache.get(change.getProject())
-          .getLabelTypes(), patchSetId, change.currentPatchSetId());
-
-      final ChangeMessage cmsg =
-          new ChangeMessage(new ChangeMessage.Key(change.getId(),
-              ChangeUtil.messageUUID(db)), uploader, patchSetId);
-      cmsg.setMessage("Patch Set " + change.currentPatchSetId().get()
-          + ": Patch Set " + patchSetId.get() + " was rebased");
-      db.changeMessages().insert(Collections.singleton(cmsg));
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-
-    return newPatchSet;
+    return db.patchSets().get(newChange.currentPatchSetId());
   }
 
   /**
@@ -443,6 +362,34 @@
     return objectId;
   }
 
+  public boolean canRebase(RevisionResource r) {
+    Repository git;
+    try {
+      git = gitManager.openRepository(r.getChange().getProject());
+    } catch (RepositoryNotFoundException err) {
+      return false;
+    } catch (IOException err) {
+      return false;
+    }
+    try {
+      findBaseRevision(
+          r.getPatchSet().getId(),
+          db,
+          r.getChange().getDest(),
+          git,
+          null,
+          null,
+          null);
+      return true;
+    } catch (IOException e) {
+      return false;
+    } catch (OrmException e) {
+      return false;
+    } finally {
+      git.close();
+    }
+  }
+
   public static boolean canDoRebase(final ReviewDb db,
       final Change change, final GitRepositoryManager gitManager,
       List<PatchSetAncestor> patchSetAncestors,
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 9a804c1..0b414f2 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
@@ -36,9 +36,14 @@
 public class AuthConfig {
   private final AuthType authType;
   private final String httpHeader;
+  private final String httpDisplaynameHeader;
+  private final String httpEmailHeader;
+  private final String registerPageUrl;
   private final boolean trustContainerAuth;
+  private final boolean enableRunAs;
   private final boolean userNameToLowerCase;
   private final boolean gitBasicAuth;
+  private final String loginUrl;
   private final String logoutUrl;
   private final String openIdSsoUrl;
   private final List<String> openIdDomains;
@@ -56,7 +61,11 @@
       throws XsrfException {
     authType = toType(cfg);
     httpHeader = cfg.getString("auth", null, "httpheader");
+    httpDisplaynameHeader = cfg.getString("auth", null, "httpdisplaynameheader");
+    httpEmailHeader = cfg.getString("auth", null, "httpemailheader");
+    loginUrl = cfg.getString("auth", null, "loginurl");
     logoutUrl = cfg.getString("auth", null, "logouturl");
+    registerPageUrl = cfg.getString("auth", null, "registerPageUrl");
     openIdSsoUrl = cfg.getString("auth", null, "openidssourl");
     openIdDomains = Arrays.asList(cfg.getStringList("auth", null, "openIdDomain"));
     trustedOpenIDs = toPatterns(cfg, "trustedOpenID");
@@ -64,6 +73,7 @@
     cookiePath = cfg.getString("auth", null, "cookiepath");
     cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
     trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
+    enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
     gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
     userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
 
@@ -122,6 +132,18 @@
     return httpHeader;
   }
 
+  public String getHttpDisplaynameHeader() {
+    return httpDisplaynameHeader;
+  }
+
+  public String getHttpEmailHeader() {
+    return httpEmailHeader;
+  }
+
+  public String getLoginUrl() {
+    return loginUrl;
+  }
+
   public String getLogoutURL() {
     return logoutUrl;
   }
@@ -164,6 +186,11 @@
     return trustContainerAuth;
   }
 
+  /** @return true if users with Run As capability can impersonate others. */
+  public boolean isRunAsEnabled() {
+    return enableRunAs;
+  }
+
   /** Whether user name should be converted to lower-case before validation */
   public boolean isUserNameToLowerCase() {
     return userNameToLowerCase;
@@ -246,4 +273,8 @@
     }
     return false;
   }
+
+  public String getRegisterPageUrl() {
+    return registerPageUrl;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
new file mode 100644
index 0000000..ca4a9d2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
@@ -0,0 +1,54 @@
+// 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.config;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.account.DefaultRealm;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.auth.AuthBackend;
+import com.google.gerrit.server.auth.InternalAuthBackend;
+import com.google.gerrit.server.auth.ldap.LdapModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+public class AuthModule extends AbstractModule {
+  private final AuthType loginType;
+
+  @Inject
+  AuthModule(AuthConfig authConfig) {
+    loginType = authConfig.getAuthType();
+  }
+
+  @Override
+  protected void configure() {
+    switch (loginType) {
+      case HTTP_LDAP:
+      case LDAP:
+      case LDAP_BIND:
+      case CLIENT_SSL_CERT_LDAP:
+        install(new LdapModule());
+        break;
+
+      case CUSTOM_EXTENSION:
+        break;
+
+      default:
+        bind(Realm.class).to(DefaultRealm.class);
+        DynamicSet.bind(binder(), AuthBackend.class).to(InternalAuthBackend.class);
+        break;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java
new file mode 100644
index 0000000..3a8bcc5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java
@@ -0,0 +1,52 @@
+// 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.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class CapabilitiesCollection implements
+    ChildCollection<ConfigResource, CapabilityResource> {
+  private final DynamicMap<RestView<CapabilityResource>> views;
+  private final Provider<ListCapabilities> list;
+
+  @Inject
+  CapabilitiesCollection(DynamicMap<RestView<CapabilityResource>> views,
+      Provider<ListCapabilities> list) {
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public RestView<ConfigResource> list() throws ResourceNotFoundException {
+    return list.get();
+  }
+
+  @Override
+  public CapabilityResource parse(ConfigResource parent, IdString id)
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<CapabilityResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
new file mode 100644
index 0000000..2e54f3e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -0,0 +1,41 @@
+// 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.config;
+
+import org.eclipse.jgit.nls.NLS;
+import org.eclipse.jgit.nls.TranslationBundle;
+
+public class CapabilityConstants extends TranslationBundle {
+  public static CapabilityConstants get() {
+    return NLS.getBundleFor(CapabilityConstants.class);
+  }
+
+  public String accessDatabase;
+  public String administrateServer;
+  public String createAccount;
+  public String createGroup;
+  public String createProject;
+  public String emailReviewers;
+  public String flushCaches;
+  public String killTask;
+  public String priority;
+  public String queryLimit;
+  public String runAs;
+  public String runGC;
+  public String streamEvents;
+  public String viewCaches;
+  public String viewConnections;
+  public String viewQueue;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityResource.java
new file mode 100644
index 0000000..7e3c87e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityResource.java
@@ -0,0 +1,23 @@
+// 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.config;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class CapabilityResource extends ConfigResource {
+  public static final TypeLiteral<RestView<CapabilityResource>> CAPABILITY_KIND =
+      new TypeLiteral<RestView<CapabilityResource>>() {};
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
new file mode 100644
index 0000000..5ed007a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
@@ -0,0 +1,52 @@
+// 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.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+
+public class ConfigCollection implements
+    RestCollection<TopLevelResource, ConfigResource> {
+  private final DynamicMap<RestView<ConfigResource>> views;
+
+  @Inject
+  ConfigCollection(DynamicMap<RestView<ConfigResource>> views) {
+    this.views = views;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() throws ResourceNotFoundException {
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public DynamicMap<RestView<ConfigResource>> views() {
+    return views;
+  }
+
+  @Override
+  public ConfigResource parse(TopLevelResource root, IdString id)
+      throws ResourceNotFoundException {
+    if (id.equals("server")) {
+      return new ConfigResource();
+    }
+    throw new ResourceNotFoundException(id);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigResource.java
new file mode 100644
index 0000000..ec0e0c2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigResource.java
@@ -0,0 +1,24 @@
+// 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.config;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class ConfigResource implements RestResource {
+  public static final TypeLiteral<RestView<ConfigResource>> CONFIG_KIND =
+      new TypeLiteral<RestView<ConfigResource>>() {};
+}
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 62c6863..6a90165 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
@@ -19,16 +19,22 @@
 import com.google.common.cache.Cache;
 import com.google.gerrit.audit.AuditModule;
 import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
@@ -44,7 +50,6 @@
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.ChangeUserName;
-import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCacheImpl;
@@ -57,15 +62,11 @@
 import com.google.gerrit.server.account.InternalGroupBackend;
 import com.google.gerrit.server.account.PerformCreateGroup;
 import com.google.gerrit.server.account.PerformRenameGroup;
-import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.UniversalGroupBackend;
 import com.google.gerrit.server.auth.AuthBackend;
-import com.google.gerrit.server.auth.InternalAuthBackend;
 import com.google.gerrit.server.auth.UniversalAuthBackend;
-import com.google.gerrit.server.auth.ldap.LdapModule;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
-import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.ChangeCache;
@@ -80,6 +81,9 @@
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
+import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
@@ -88,10 +92,11 @@
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
-import com.google.gerrit.server.mail.RebasedPatchSetSender;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.mail.VelocityRuntimeProvider;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
+import com.google.gerrit.server.patch.PatchScriptFactory;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.ChangeControl;
@@ -105,7 +110,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
@@ -114,45 +118,27 @@
 import com.google.inject.TypeLiteral;
 
 import org.apache.velocity.runtime.RuntimeInstance;
-import org.eclipse.jgit.lib.Config;
 
 import java.util.List;
 
 
 /** Starts global state with standard dependencies. */
 public class GerritGlobalModule extends FactoryModule {
-  private final AuthType loginType;
+  private final AuthModule authModule;
 
   @Inject
-  GerritGlobalModule(final AuthConfig authConfig,
-      @GerritServerConfig final Config config) {
-    loginType = authConfig.getAuthType();
+  GerritGlobalModule(AuthModule authModule) {
+    this.authModule = authModule;
   }
 
   @Override
   protected void configure() {
-    switch (loginType) {
-      case HTTP_LDAP:
-      case LDAP:
-      case LDAP_BIND:
-      case CLIENT_SSL_CERT_LDAP:
-        install(new LdapModule());
-        break;
-
-      case CUSTOM_EXTENSION:
-        break;
-
-      default:
-        bind(Realm.class).to(DefaultRealm.class);
-        DynamicSet.bind(binder(), AuthBackend.class).to(InternalAuthBackend.class);
-        break;
-    }
-
     bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in(
         SINGLETON);
 
     bind(IdGenerator.class);
     bind(RulesCache.class);
+    install(authModule);
     install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
     install(GroupCacheImpl.module());
@@ -164,6 +150,7 @@
     install(ChangeCache.module());
 
     install(new AccessControlModule());
+    install(new CmdLineParserModule());
     install(new EmailModule());
     install(new GitModule());
     install(new PrologModule());
@@ -171,7 +158,6 @@
     install(ThreadLocalRequestContext.module());
 
     bind(AccountResolver.class);
-    bind(ChangeQueryRewriter.class);
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(AddReviewerSender.Factory.class);
@@ -186,12 +172,13 @@
     factory(MergedSender.Factory.class);
     factory(MergeFailSender.Factory.class);
     factory(MergeUtil.Factory.class);
+    factory(PatchScriptFactory.Factory.class);
     factory(PerformCreateGroup.Factory.class);
     factory(PerformRenameGroup.Factory.class);
     factory(PluginUser.Factory.class);
     factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
-    factory(RebasedPatchSetSender.Factory.class);
+    factory(RegisterNewEmailSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
     factory(PerformCreateProject.Factory.class);
     factory(GarbageCollection.Factory.class);
@@ -218,7 +205,6 @@
     bind(TransferConfig.class);
 
     bind(ApprovalsUtil.class);
-    bind(ChangeInserter.class);
     bind(ChangeMergeQueue.class).in(SINGLETON);
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
     factory(ReloadSubmitQueueOp.Factory.class);
@@ -236,24 +222,35 @@
     bind(AccountControl.Factory.class);
 
     install(new AuditModule());
+    install(new com.google.gerrit.server.access.Module());
     install(new com.google.gerrit.server.account.Module());
     install(new com.google.gerrit.server.change.Module());
+    install(new com.google.gerrit.server.config.Module());
     install(new com.google.gerrit.server.group.Module());
     install(new com.google.gerrit.server.project.Module());
 
     bind(GitReferenceUpdated.class);
     DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
     DynamicSet.setOf(binder(), CacheRemovalListener.class);
+    DynamicMap.mapOf(binder(), CapabilityDefinition.class);
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
+    DynamicSet.setOf(binder(), ProjectDeletedListener.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
     DynamicSet.setOf(binder(), ChangeListener.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
+    DynamicSet.setOf(binder(), MergeValidationListener.class);
     DynamicItem.itemOf(binder(), AvatarProvider.class);
+    DynamicSet.setOf(binder(), LifecycleListener.class);
+    DynamicSet.setOf(binder(), TopMenu.class);
+    DynamicMap.mapOf(binder(), DownloadScheme.class);
+    DynamicMap.mapOf(binder(), DownloadCommand.class);
 
     bind(AnonymousUser.class);
 
     factory(CommitValidators.Factory.class);
+    factory(MergeValidators.Factory.class);
+    factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
 
     bind(AccountManager.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
new file mode 100644
index 0000000..f618959c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
@@ -0,0 +1,30 @@
+// 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.config;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetVersion implements RestReadView<ConfigResource> {
+  @Override
+  public String apply(ConfigResource resource) throws ResourceNotFoundException {
+    String version = Version.getVersion();
+    if (version == null) {
+      throw new ResourceNotFoundException();
+    }
+    return version;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 8b517a3..641a48f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -25,8 +27,10 @@
 public class GitReceivePackGroupsProvider extends GroupSetProvider {
   @Inject
   public GitReceivePackGroupsProvider(GroupBackend gb,
-      @GerritServerConfig Config config) {
-    super(gb, config, "receive", null, "allowGroup");
+      @GerritServerConfig Config config,
+      ThreadLocalRequestContext threadContext,
+      ServerRequestContext serverCtx) {
+    super(gb, config, threadContext, serverCtx, "receive", null, "allowGroup");
 
     // If no group was set, default to "registered users"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index c519902..edae46b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -26,8 +28,10 @@
 public class GitUploadPackGroupsProvider extends GroupSetProvider {
   @Inject
   public GitUploadPackGroupsProvider(GroupBackend gb,
-      @GerritServerConfig Config config) {
-    super(gb, config, "upload", null, "allowGroup");
+      @GerritServerConfig Config config,
+      ThreadLocalRequestContext threadContext,
+      ServerRequestContext serverCtx) {
+    super(gb, config, threadContext, serverCtx, "upload", null, "allowGroup");
 
     // If no group was set, default to "registered users" and "anonymous"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 5fa243b..5c3ec39 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -19,6 +19,9 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -37,19 +40,26 @@
 
   @Inject
   protected GroupSetProvider(GroupBackend groupBackend,
-      @GerritServerConfig Config config, String section,
+      @GerritServerConfig Config config,
+      ThreadLocalRequestContext threadContext,
+      ServerRequestContext serverCtx, String section,
       String subsection, String name) {
-    String[] groupNames = config.getStringList(section, subsection, name);
-    ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
-    for (String n : groupNames) {
-      GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
-      if (g == null) {
-        log.warn("Group \"{0}\" not in database, skipping.", n);
-      } else {
-        builder.add(g.getUUID());
+    RequestContext ctx = threadContext.setContext(serverCtx);
+    try {
+      String[] groupNames = config.getStringList(section, subsection, name);
+      ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
+      for (String n : groupNames) {
+        GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
+        if (g == null) {
+          log.warn("Group \"{}\" not in database, skipping.", n);
+        } else {
+          builder.add(g.getUUID());
+        }
       }
+      groupIds = builder.build();
+    } finally {
+      threadContext.setContext(ctx);
     }
-    groupIds = builder.build();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
new file mode 100644
index 0000000..c64f786
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
@@ -0,0 +1,101 @@
+// 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.config;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+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.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+/** List capabilities visible to the calling user. */
+public class ListCapabilities implements RestReadView<ConfigResource> {
+  private static final Logger log = LoggerFactory.getLogger(ListCapabilities.class);
+  private final DynamicMap<CapabilityDefinition> pluginCapabilities;
+
+  @Inject
+  public ListCapabilities(DynamicMap<CapabilityDefinition> pluginCapabilities) {
+    this.pluginCapabilities = pluginCapabilities;
+  }
+
+  @Override
+  public Map<String, CapabilityInfo> apply(ConfigResource resource)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      IllegalArgumentException, SecurityException, IllegalAccessException,
+      NoSuchFieldException {
+    Map<String, CapabilityInfo> output = Maps.newTreeMap();
+    collectCoreCapabilities(output);
+    collectPluginCapabilities(output);
+    return output;
+  }
+
+  private void collectCoreCapabilities(Map<String, CapabilityInfo> output)
+      throws IllegalAccessException, NoSuchFieldException {
+    Class<? extends CapabilityConstants> bundleClass =
+        CapabilityConstants.get().getClass();
+    CapabilityConstants c = CapabilityConstants.get();
+    for (String id : GlobalCapability.getAllNames()) {
+      String name = (String) bundleClass.getField(id).get(c);
+      output.put(id, new CapabilityInfo(id, name));
+    }
+  }
+
+  private void collectPluginCapabilities(Map<String, CapabilityInfo> output) {
+    for (String pluginName : pluginCapabilities.plugins()) {
+      if (!isPluginNameSane(pluginName)) {
+        log.warn(String.format(
+            "Plugin name %s must match [A-Za-z0-9-]+ to use capabilities;"
+            + " rename the plugin",
+            pluginName));
+        continue;
+      }
+      for (Map.Entry<String, Provider<CapabilityDefinition>> entry :
+          pluginCapabilities.byPlugin(pluginName).entrySet()) {
+        String id = String.format("%s-%s", pluginName, entry.getKey());
+        output.put(id, new CapabilityInfo(
+            id,
+            entry.getValue().get().getDescription()));
+      }
+    }
+  }
+
+  private static boolean isPluginNameSane(String pluginName) {
+    return CharMatcher.JAVA_LETTER_OR_DIGIT
+        .or(CharMatcher.is('-'))
+        .matchesAllOf(pluginName);
+  }
+
+  public static class CapabilityInfo {
+    final String kind = "gerritcodereview#capability";
+    public String id;
+    public String name;
+
+    public CapabilityInfo(String id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTopMenus.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTopMenus.java
new file mode 100644
index 0000000..68ae5c1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTopMenus.java
@@ -0,0 +1,41 @@
+// 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.config;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.inject.Inject;
+
+import java.util.List;
+
+class ListTopMenus implements RestReadView<ConfigResource> {
+  private final DynamicSet<TopMenu> extensions;
+
+  @Inject
+  ListTopMenus(DynamicSet<TopMenu> extensions) {
+    this.extensions = extensions;
+  }
+
+  @Override
+  public Object apply(ConfigResource resource) {
+    List<TopMenu.MenuEntry> entries = Lists.newArrayList();
+    for (TopMenu extension : extensions) {
+      entries.addAll(extension.getEntries());
+    }
+    return entries;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
new file mode 100644
index 0000000..57bbbf3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -0,0 +1,34 @@
+// 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.config;
+
+import static com.google.gerrit.server.config.CapabilityResource.CAPABILITY_KIND;
+import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.config.TopMenuResource.TOP_MENU_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class Module extends RestApiModule {
+  @Override
+  protected void configure() {
+    DynamicMap.mapOf(binder(), CONFIG_KIND);
+    DynamicMap.mapOf(binder(), TOP_MENU_KIND);
+    DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+    child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
+    child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
+    get(CONFIG_KIND, "version").to(GetVersion.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
new file mode 100644
index 0000000..279bf8e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
@@ -0,0 +1,98 @@
+// 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.config;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectState;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Arrays;
+import java.util.Set;
+
+public class PluginConfig {
+  private static final String PLUGIN = "plugin";
+
+  private final String pluginName;
+  private Config cfg;
+  private final ProjectConfig projectConfig;
+
+  public PluginConfig(String pluginName, Config cfg) {
+    this(pluginName, cfg, null);
+  }
+
+  public PluginConfig(String pluginName, Config cfg, ProjectConfig projectConfig) {
+    this.pluginName = pluginName;
+    this.cfg = cfg;
+    this.projectConfig = projectConfig;
+  }
+
+  PluginConfig withInheritance(ProjectState.Factory projectStateFactory) {
+    if (projectConfig == null) {
+      return this;
+    }
+
+    ProjectState state = projectStateFactory.create(projectConfig);
+    ProjectState parent = Iterables.getFirst(state.parents(), null);
+    if (parent != null) {
+      PluginConfig parentPluginConfig =
+          parent.getConfig().getPluginConfig(pluginName)
+              .withInheritance(projectStateFactory);
+      Set<String> allNames = cfg.getNames(PLUGIN, pluginName);
+      cfg = new Config(cfg);
+      for (String name : parentPluginConfig.cfg.getNames(PLUGIN, pluginName)) {
+        if (!allNames.contains(name)) {
+          cfg.setStringList(PLUGIN, pluginName, name, Arrays
+              .asList(parentPluginConfig.cfg.getStringList(PLUGIN, pluginName, name)));
+        }
+      }
+    }
+    return this;
+  }
+
+  public String getString(String name) {
+    return cfg.getString(PLUGIN, pluginName, name);
+  }
+
+  public String getString(String name, String defaultValue) {
+    return Objects.firstNonNull(cfg.getString(PLUGIN, pluginName, name), defaultValue);
+  }
+
+  public String[] getStringList(String name) {
+    return cfg.getStringList(PLUGIN, pluginName, name);
+  }
+
+  public int getInt(String name, int defaultValue) {
+    return cfg.getInt(PLUGIN, pluginName, name, defaultValue);
+  }
+
+  public long getLong(String name, long defaultValue) {
+    return cfg.getLong(PLUGIN, pluginName, name, defaultValue);
+  }
+
+  public boolean getBoolean(String name, boolean defaultValue) {
+    return cfg.getBoolean(PLUGIN, pluginName, name, defaultValue);
+  }
+
+  public <T extends Enum<?>> T getEnum(String name, T defaultValue) {
+    return cfg.getEnum(PLUGIN, pluginName, name, defaultValue);
+  }
+
+  public <T extends Enum<?>> T getEnum(T[] all, String name, T defaultValue) {
+    return cfg.getEnum(all, PLUGIN, pluginName, name, defaultValue);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
new file mode 100644
index 0000000..294d8a5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -0,0 +1,131 @@
+// 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.config;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class PluginConfigFactory {
+  private final Config cfg;
+  private final ProjectCache projectCache;
+  private final ProjectState.Factory projectStateFactory;
+
+  @Inject
+  PluginConfigFactory(@GerritServerConfig Config cfg,
+      ProjectCache projectCache, ProjectState.Factory projectStateFactory) {
+    this.cfg = cfg;
+    this.projectCache = projectCache;
+    this.projectStateFactory = projectStateFactory;
+  }
+
+  /**
+   * Returns the configuration for the specified plugin that is stored in the
+   * 'gerrit.config' file.
+   *
+   * The returned plugin configuration provides access to all parameters of the
+   * 'gerrit.config' file that are set in the 'plugin' subsection of the
+   * specified plugin.
+   *
+   * E.g.:
+   *   [plugin "my-plugin"]
+   *     myKey = myValue
+   *
+   * @param pluginName the name of the plugin for which the configuration should
+   *        be returned
+   * @return the plugin configuration from the 'gerrit.config' file
+   */
+  public PluginConfig getFromGerritConfig(String pluginName) {
+    return new PluginConfig(pluginName, cfg);
+  }
+
+  /**
+   * Returns the configuration for the specified plugin that is stored in the
+   * 'project.config' file of the specified project.
+   *
+   * The returned plugin configuration provides access to all parameters of the
+   * 'project.config' file that are set in the 'plugin' subsection of the
+   * specified plugin.
+   *
+   * E.g.:
+   *   [plugin "my-plugin"]
+   *     myKey = myValue
+   *
+   * @param projectName the name of the project for which the plugin
+   *        configuration should be returned
+   * @param pluginName the name of the plugin for which the configuration should
+   *        be returned
+   * @return the plugin configuration from the 'project.config' file of the
+   *         specified project
+   * @throws NoSuchProjectException thrown if the specified project does not
+   *         exist
+   */
+  public PluginConfig getFromProjectConfig(Project.NameKey projectName,
+      String pluginName) throws NoSuchProjectException {
+    ProjectState projectState = projectCache.get(projectName);
+    if (projectState == null) {
+      throw new NoSuchProjectException(projectName);
+    }
+    return projectState.getConfig().getPluginConfig(pluginName);
+  }
+
+  /**
+   * Returns the configuration for the specified plugin that is stored in the
+   * 'project.config' file of the specified project. Parameters which are not
+   * set in the 'project.config' of this project are inherited from the parent
+   * project's 'project.config' files.
+   *
+   * The returned plugin configuration provides access to all parameters of the
+   * 'project.config' file that are set in the 'plugin' subsection of the
+   * specified plugin.
+   *
+   * E.g.:
+   * child project:
+   *   [plugin "my-plugin"]
+   *     myKey = childValue
+   *
+   * parent project:
+   *   [plugin "my-plugin"]
+   *     myKey = parentValue
+   *     anotherKey = someValue
+   *
+   * return:
+   *   [plugin "my-plugin"]
+   *     myKey = childValue
+   *     anotherKey = someValue
+   *
+   * @param projectName the name of the project for which the plugin
+   *        configuration should be returned
+   * @param pluginName the name of the plugin for which the configuration should
+   *        be returned
+   * @return the plugin configuration from the 'project.config' file of the
+   *         specified project with inherited non-set parameters from the
+   *         parent projects
+   * @throws NoSuchProjectException thrown if the specified project does not
+   *         exist
+   */
+  public PluginConfig getFromProjectConfigWithInheritance(
+      Project.NameKey projectName, String pluginName)
+      throws NoSuchProjectException {
+    return getFromProjectConfig(projectName, pluginName).withInheritance(
+        projectStateFactory);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 6622b0f..0189de3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -33,7 +35,9 @@
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
   @Inject
   public ProjectOwnerGroupsProvider(GroupBackend gb,
-      @GerritServerConfig final Config config) {
-    super(gb, config, "repository", "*", "ownerGroup");
+      @GerritServerConfig final Config config,
+      ThreadLocalRequestContext context,
+      ServerRequestContext serverCtx) {
+    super(gb, config, context, serverCtx, "repository", "*", "ownerGroup");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 2116c0c..9d7c54a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -40,6 +40,7 @@
   public final File hooks_dir;
   public final File static_dir;
   public final File themes_dir;
+  public final File index_dir;
 
   public final File gerrit_sh;
   public final File gerrit_war;
@@ -77,6 +78,7 @@
     hooks_dir = new File(site_path, "hooks");
     static_dir = new File(site_path, "static");
     themes_dir = new File(site_path, "themes");
+    index_dir = new File(site_path, "index");
 
     gerrit_sh = new File(bin_dir, "gerrit.sh");
     gerrit_war = new File(bin_dir, "gerrit.war");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuCollection.java
new file mode 100644
index 0000000..55fd2df
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuCollection.java
@@ -0,0 +1,52 @@
+// 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.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class TopMenuCollection implements
+    ChildCollection<ConfigResource, TopMenuResource> {
+  private final DynamicMap<RestView<TopMenuResource>> views;
+  private final Provider<ListTopMenus> list;
+
+  @Inject
+  TopMenuCollection(DynamicMap<RestView<TopMenuResource>> views,
+      Provider<ListTopMenus> list) {
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public RestView<ConfigResource> list() throws ResourceNotFoundException {
+    return list.get();
+  }
+
+  @Override
+  public TopMenuResource parse(ConfigResource parent, IdString id)
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<TopMenuResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuResource.java
new file mode 100644
index 0000000..bca6331
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TopMenuResource.java
@@ -0,0 +1,23 @@
+// 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.config;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class TopMenuResource extends ConfigResource {
+  public static final TypeLiteral<RestView<TopMenuResource>> TOP_MENU_KIND =
+      new TypeLiteral<RestView<TopMenuResource>>() {};
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
index a82e23b..bfb6db5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
@@ -14,7 +14,13 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.Sets;
+
+import org.eclipse.jgit.revwalk.FooterLine;
+
 import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
 
 public class TrackingFooters {
   protected List<TrackingFooter> trackingFooters;
@@ -26,4 +32,24 @@
   public List<TrackingFooter> getTrackingFooters() {
     return trackingFooters;
   }
+
+  public Set<String> extract(List<FooterLine> lines) {
+    Set<String> r = Sets.newHashSet();
+    for (FooterLine footer : lines) {
+      for (TrackingFooter config : trackingFooters) {
+        if (footer.matches(config.footerKey())) {
+          Matcher m = config.match().matcher(footer.getValue());
+          while (m.find()) {
+            String id = m.groupCount() > 0
+                ? m.group(1)
+                : m.group();
+            if (!id.isEmpty()) {
+              r.add(id);
+            }
+          }
+        }
+      }
+    }
+    return r;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
index bd1c0d7..17c9060 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UrlEncoded;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.ProvisionException;
@@ -213,7 +214,7 @@
       throws ContactInformationStoreException {
     Timestamp on = account.getContactFiledOn();
     if (on == null) {
-      on = new Timestamp(System.currentTimeMillis());
+      on = TimeUtil.nowTs();
     }
 
     final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index 79d82e3..91df974 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -24,6 +24,7 @@
   public AccountAttribute uploader;
   public Long createdOn;
   public AccountAttribute author;
+  public boolean isDraft;
 
   public List<ApprovalAttribute> approvals;
   public List<PatchSetCommentAttribute> comments;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 63bfa71..98e803f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -45,12 +46,14 @@
 import com.google.gerrit.server.data.SubmitLabelAttribute;
 import com.google.gerrit.server.data.SubmitRecordAttribute;
 import com.google.gerrit.server.data.TrackingIdAttribute;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -67,8 +70,6 @@
 import java.util.List;
 import java.util.Map;
 
-import javax.annotation.Nullable;
-
 @Singleton
 public class EventFactory {
   private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
@@ -78,19 +79,24 @@
   private final SchemaFactory<ReviewDb> schema;
   private final PatchSetInfoFactory psInfoFactory;
   private final PersonIdent myIdent;
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
 
   @Inject
   EventFactory(AccountCache accountCache,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       PatchSetInfoFactory psif,
       PatchListCache patchListCache, SchemaFactory<ReviewDb> schema,
-      @GerritPersonIdent PersonIdent myIdent) {
+      @GerritPersonIdent PersonIdent myIdent,
+      Provider<ReviewDb> db, GitRepositoryManager repoManager) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
     this.patchListCache = patchListCache;
     this.schema = schema;
     this.psInfoFactory = psif;
     this.myIdent = myIdent;
+    this.db = db;
+    this.repoManager = repoManager;
   }
 
   /**
@@ -108,8 +114,15 @@
     a.id = change.getKey().get();
     a.number = change.getId().toString();
     a.subject = change.getSubject();
+    try {
+      a.commitMessage = new ChangeData(change).commitMessage(repoManager, db);
+    } catch (Exception e) {
+      log.error("Error while getting full commit message for"
+          + " change " + a.number);
+    }
     a.url = getChangeUrl(change);
     a.owner = asAccountAttribute(change.getOwner());
+    a.status = change.getStatus();
     return a;
   }
 
@@ -117,7 +130,8 @@
    * Create a RefUpdateAttribute for the given old ObjectId, new ObjectId, and
    * branch that is suitable for serialization to JSON.
    *
-   * @param refUpdate
+   * @param oldId
+   * @param newId
    * @param refName
    * @return object suitable for serialization to JSON
    */
@@ -141,7 +155,6 @@
     a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
     a.sortKey = change.getSortKey();
     a.open = change.getStatus().isOpen();
-    a.status = change.getStatus();
   }
 
   /**
@@ -361,6 +374,7 @@
     p.ref = patchSet.getRefName();
     p.uploader = asAccountAttribute(patchSet.getUploader());
     p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
+    p.isDraft = patchSet.isDraft();
     final PatchSet.Id pId = patchSet.getId();
     try {
       final ReviewDb db = schema.open();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
new file mode 100644
index 0000000..e725eac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
@@ -0,0 +1,25 @@
+// 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.events;
+
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+
+public class TopicChangedEvent extends ChangeEvent {
+  public final String type = "topic-changed";
+  public ChangeAttribute change;
+  public AccountAttribute changer;
+  public String oldTopic;
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 49c90bd..9e48e81 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.extensions.events;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -22,11 +21,15 @@
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
-import java.util.List;
 
 public class GitReferenceUpdated {
+  private static final Logger log = LoggerFactory
+      .getLogger(GitReferenceUpdated.class);
+
   public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated(
       Collections.<GitReferenceUpdatedListener> emptyList());
 
@@ -52,7 +55,11 @@
     ObjectId n = newObjectId != null ? newObjectId : ObjectId.zeroId();
     Event event = new Event(project, ref, o.name(), n.name());
     for (GitReferenceUpdatedListener l : listeners) {
-      l.onGitReferenceUpdated(event);
+      try {
+        l.onGitReferenceUpdated(event);
+      } catch (RuntimeException e) {
+        log.warn("Failure in GitReferenceUpdatedListener", e);
+      }
     }
   }
 
@@ -76,25 +83,18 @@
     }
 
     @Override
-    public List<GitReferenceUpdatedListener.Update> getUpdates() {
-      GitReferenceUpdatedListener.Update update =
-          new GitReferenceUpdatedListener.Update() {
-            @Override
-            public String getRefName() {
-              return ref;
-            }
+    public String getRefName() {
+      return ref;
+    }
 
-            @Override
-            public String getOldObjectId() {
-              return oldObjectId;
-            }
+    @Override
+    public String getOldObjectId() {
+      return oldObjectId;
+    }
 
-            @Override
-            public String getNewObjectId() {
-              return newObjectId;
-            }
-          };
-      return ImmutableList.of(update);
+    @Override
+    public String getNewObjectId() {
+      return newObjectId;
     }
   }
 }
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
new file mode 100644
index 0000000..7dd8d47
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -0,0 +1,141 @@
+// 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.extensions.webui;
+
+import com.google.common.base.Function;
+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;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityUtils;
+import com.google.inject.Provider;
+
+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);
+
+  public static Predicate<UiAction.Description> enabled() {
+    return new Predicate<UiAction.Description>() {
+      @Override
+      public boolean apply(UiAction.Description input) {
+        return input.isEnabled();
+      }
+    };
+  }
+
+  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,
+      Provider<CurrentUser> userProvider) {
+    return from(collection.views(), resource, userProvider);
+  }
+
+  public static <R extends RestResource> Iterable<UiAction.Description> from(
+      DynamicMap<RestView<R>> views,
+      final R resource,
+      final Provider<CurrentUser> userProvider) {
+    return Iterables.filter(
+      Iterables.transform(
+        views,
+        new Function<DynamicMap.Entry<RestView<R>>, UiAction.Description> () {
+          @Override
+          @Nullable
+          public UiAction.Description apply(DynamicMap.Entry<RestView<R>> e) {
+            int d = e.getExportName().indexOf('.');
+            if (d < 0) {
+              return null;
+            }
+
+            RestView<R> view;
+            try {
+              view = e.getProvider().get();
+            } catch (RuntimeException err) {
+              log.error(String.format(
+                  "error creating view %s.%s",
+                  e.getPluginName(), e.getExportName()), err);
+              return null;
+            }
+
+            if (!(view instanceof UiAction)) {
+              return null;
+            }
+
+            try {
+              CapabilityUtils.checkRequiresCapability(userProvider,
+                  e.getPluginName(), view.getClass());
+            } catch (AuthException exc) {
+              return null;
+            }
+
+            UiAction.Description dsc =
+                ((UiAction<R>) view).getDescription(resource);
+            if (dsc == null || !dsc.isVisible()) {
+              return null;
+            }
+
+            String name = e.getExportName().substring(d + 1);
+            PrivateInternals_UiActionDescription.setMethod(
+                dsc,
+                e.getExportName().substring(0, d));
+            PrivateInternals_UiActionDescription.setId(
+                dsc,
+                "gerrit".equals(e.getPluginName())
+                  ? name
+                  : e.getPluginName() + '~' + name);
+            return dsc;
+          }
+        }),
+      Predicates.notNull());
+  }
+
+  private UiActions() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
index 6f71936..32dc303 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.git;
 
@@ -72,11 +72,8 @@
 
   @Override
   public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    for (GitReferenceUpdatedListener.Update u : event.getUpdates()) {
-      if (u.getRefName().startsWith("refs/changes/")) {
-        cache.invalidate(new Project.NameKey(event.getProjectName()));
-        break;
-      }
+    if (event.getRefName().startsWith("refs/changes/")) {
+      cache.invalidate(new Project.NameKey(event.getProjectName()));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index 12219e2..a6b0a12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -155,7 +156,7 @@
   @Override
   public synchronized void recheckAfter(final Branch.NameKey branch,
       final long delay, final TimeUnit delayUnit) {
-    final long now = System.currentTimeMillis();
+    final long now = TimeUtil.nowMs();
     final long at = now + MILLISECONDS.convert(delay, delayUnit);
     RecheckJob e = recheck.get(branch);
     if (e == null) {
@@ -216,7 +217,7 @@
   }
 
   private synchronized void recheck(final RecheckJob e) {
-    final long remainingDelay = e.recheckAt - System.currentTimeMillis();
+    final long remainingDelay = e.recheckAt - TimeUtil.nowMs();
     if (MILLISECONDS.convert(10, SECONDS) < remainingDelay) {
       // Woke up too early, the job deadline was pushed back.
       // Reschedule for the new deadline. We allow for a small
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
index ce5308f..ce7a708 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -32,7 +33,6 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -148,8 +148,9 @@
     final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
 
     final CodeReviewCommit newCommit =
-        args.mergeUtil.createCherryPickFromCommit(args.repo, args.inserter, mergeTip, n,
-            cherryPickCommitterIdent, cherryPickCmtMsg, args.rw);
+        (CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
+            args.inserter, mergeTip, n, cherryPickCommitterIdent,
+            cherryPickCmtMsg, args.rw);
 
     if (newCommit == null) {
         return null;
@@ -158,7 +159,7 @@
     PatchSet.Id id =
         ChangeUtil.nextPatchSetId(args.repo, n.change.currentPatchSetId());
     final PatchSet ps = new PatchSet(id);
-    ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+    ps.setCreatedOn(TimeUtil.nowTs());
     ps.setUploader(submitAudit.getAccountId());
     ps.setRevision(new RevId(newCommit.getId().getName()));
     insertAncestors(args.db, ps.getId(), newCommit);
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 86d79e0..311856d 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
@@ -24,7 +24,7 @@
 import java.util.List;
 
 /** Extended commit entity with code review specific metadata. */
-class CodeReviewCommit extends RevCommit {
+public class CodeReviewCommit extends RevCommit {
   static CodeReviewCommit error(final CommitMergeStatus s) {
     final CodeReviewCommit r = new CodeReviewCommit(ObjectId.zeroId());
     r.statusCode = s;
@@ -59,7 +59,7 @@
   /** Commits which are missing ancestors of this commit. */
   List<CodeReviewCommit> missing;
 
-  CodeReviewCommit(final AnyObjectId id) {
+  public CodeReviewCommit(final AnyObjectId id) {
     super(id);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
index 685e87c..24af14d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -21,8 +21,6 @@
 
 import org.eclipse.jgit.api.GarbageCollectCommand;
 import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
@@ -33,7 +31,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.List;
 import java.util.Properties;
@@ -91,15 +88,7 @@
         result.addError(new GarbageCollectionResult.Error(
             GarbageCollectionResult.Error.Type.REPOSITORY_NOT_FOUND,
             p));
-      } catch (IOException e) {
-        logGcError(writer, p, e);
-        result.addError(new GarbageCollectionResult.Error(
-            GarbageCollectionResult.Error.Type.GC_FAILED, p));
-      } catch (GitAPIException e) {
-        logGcError(writer, p, e);
-        result.addError(new GarbageCollectionResult.Error(
-            GarbageCollectionResult.Error.Type.GC_FAILED, p));
-      } catch (JGitInternalException e) {
+      } catch (Exception e) {
         logGcError(writer, p, e);
         result.addError(new GarbageCollectionResult.Error(
             GarbageCollectionResult.Error.Type.GC_FAILED, p));
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 1ca74b1..c51a6ec 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
@@ -164,10 +164,23 @@
       // on disk; for instance when the project has been created directly on the
       // file-system through replication.
       //
-      if (FileKey.resolve(gitDirOf(name), FS.DETECTED) != null) {
-        onCreateProject(name);
+      if (!name.get().endsWith(Constants.DOT_GIT_EXT)) {
+        if (FileKey.resolve(gitDirOf(name), FS.DETECTED) != null) {
+          onCreateProject(name);
+        } else {
+          throw new RepositoryNotFoundException(gitDirOf(name));
+        }
       } else {
-        throw new RepositoryNotFoundException(gitDirOf(name));
+        final File directory = gitDirOf(name);
+        if (FileKey.isGitRepository(new File(directory, Constants.DOT_GIT),
+            FS.DETECTED)) {
+          onCreateProject(name);
+        } else if (FileKey.isGitRepository(new File(directory.getParentFile(),
+            directory.getName() + Constants.DOT_GIT_EXT), FS.DETECTED)) {
+          onCreateProject(name);
+        } else {
+          throw new RepositoryNotFoundException(gitDirOf(name));
+        }
       }
     }
     final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
@@ -357,7 +370,9 @@
 
     for (File f : ls) {
       String fileName = f.getName();
-      if (FileKey.isGitRepository(f, FS.DETECTED)) {
+      if (fileName.equals(Constants.DOT_GIT)) {
+        // Skip repositories named only `.git`
+      } else if (FileKey.isGitRepository(f, FS.DETECTED)) {
         Project.NameKey nameKey = getProjectName(prefix, fileName);
         if (isUnreasonableName(nameKey)) {
           log.warn("Ignoring unreasonably named repository " + f.getAbsolutePath());
@@ -374,10 +389,7 @@
   private Project.NameKey getProjectName(final String prefix,
       final String fileName) {
     final String projectName;
-    if (fileName.equals(Constants.DOT_GIT)) {
-      projectName = prefix.substring(0, prefix.length() - 1);
-
-    } else if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
+    if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
       int newLen = fileName.length() - Constants.DOT_GIT_EXT.length();
       projectName = prefix + fileName.substring(0, newLen);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
index 493a40f..2992bd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
@@ -25,10 +25,15 @@
   }
 
   @Override
-  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
+      List<CodeReviewCommit> toMerge) throws MergeException {
     args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
 
+    if (mergeTip == null) {
+      // The branch is unborn. Take a fast-forward resolution to
+      // create the branch.
+      mergeTip = toMerge.remove(0);
+    }
     CodeReviewCommit newMergeTip = mergeTip;
     while (!toMerge.isEmpty()) {
       newMergeTip =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
index c31edb2..3a326fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
@@ -25,9 +25,16 @@
   }
 
   @Override
-  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
+      List<CodeReviewCommit> toMerge) throws MergeException {
     args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+
+    if (mergeTip == null) {
+      // The branch is unborn. Take a fast-forward resolution to
+      // create the branch.
+      mergeTip = toMerge.remove(0);
+    }
+
     CodeReviewCommit newMergeTip =
         args.mergeUtil.getFirstFastForward(mergeTip, args.rw, toMerge);
 
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 3afbdd6..cf8a16b 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
@@ -15,17 +15,23 @@
 package com.google.gerrit.server.git;
 
 import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
-import static java.util.concurrent.TimeUnit.DAYS;
+
+import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+
 import com.google.common.base.Objects;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -38,11 +44,14 @@
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.validators.MergeValidationException;
+import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -53,8 +62,8 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.AtomicUpdate;
-import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -85,7 +94,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 
 /**
@@ -116,8 +124,8 @@
   private static final long LOCK_FAILURE_RETRY_DELAY =
       MILLISECONDS.convert(15, SECONDS);
 
-  private static final long DUPLICATE_MESSAGE_INTERVAL =
-      MILLISECONDS.convert(1, DAYS);
+  private static final long MAX_SUBMIT_WINDOW =
+      MILLISECONDS.convert(12, HOURS);
 
   private final GitRepositoryManager repoManager;
   private final SchemaFactory<ReviewDb> schemaFactory;
@@ -130,12 +138,14 @@
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final MergeQueue mergeQueue;
+  private final MergeValidators.Factory mergeValidatorsFactory;
 
   private final Branch.NameKey destBranch;
   private ProjectState destProject;
   private final ListMultimap<SubmitType, CodeReviewCommit> toMerge;
   private final List<CodeReviewCommit> potentiallyStillSubmittable;
   private final Map<Change.Id, CodeReviewCommit> commits;
+  private final List<Change> toUpdate;
   private ReviewDb db;
   private Repository repo;
   private RevWalk rw;
@@ -152,7 +162,7 @@
   private final SubmoduleOp.Factory subOpFactory;
   private final WorkQueue workQueue;
   private final RequestScopePropagator requestScopePropagator;
-  private final AllProjectsName allProjectsName;
+  private final ChangeIndexer indexer;
 
   @Inject
   MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
@@ -168,7 +178,8 @@
       final SubmoduleOp.Factory subOpFactory,
       final WorkQueue workQueue,
       final RequestScopePropagator requestScopePropagator,
-      final AllProjectsName allProjectsName) {
+      final ChangeIndexer indexer,
+      final MergeValidators.Factory mergeValidatorsFactory) {
     repoManager = grm;
     schemaFactory = sf;
     labelNormalizer = fs;
@@ -187,66 +198,13 @@
     this.subOpFactory = subOpFactory;
     this.workQueue = workQueue;
     this.requestScopePropagator = requestScopePropagator;
-    this.allProjectsName = allProjectsName;
+    this.indexer = indexer;
+    this.mergeValidatorsFactory = mergeValidatorsFactory;
     destBranch = branch;
     toMerge = ArrayListMultimap.create();
     potentiallyStillSubmittable = new ArrayList<CodeReviewCommit>();
     commits = new HashMap<Change.Id, CodeReviewCommit>();
-  }
-
-  public void verifyMergeability(Change change) throws NoSuchProjectException {
-    try {
-      setDestProject();
-      openRepository();
-      final Ref destBranchRef = repo.getRef(destBranch.get());
-
-      // Test mergeability of the change if the last merged sha1
-      // in the branch is different from the last sha1
-      // the change was tested against.
-      if ((destBranchRef == null && change.getLastSha1MergeTested() == null)
-          || change.getLastSha1MergeTested() == null
-          || (destBranchRef != null && !destBranchRef.getObjectId().getName()
-              .equals(change.getLastSha1MergeTested().get()))) {
-        openSchema();
-        openBranch();
-        validateChangeList(Collections.singletonList(change));
-        if (!toMerge.isEmpty()) {
-          final Entry<SubmitType, CodeReviewCommit> e =
-              toMerge.entries().iterator().next();
-          final boolean isMergeable =
-              createStrategy(e.getKey()).dryRun(branchTip, e.getValue());
-
-          // update sha1 tested merge.
-          if (destBranchRef != null) {
-            change.setLastSha1MergeTested(new RevId(destBranchRef
-                .getObjectId().getName()));
-          } else {
-            change.setLastSha1MergeTested(new RevId(""));
-          }
-          change.setMergeable(isMergeable);
-          db.changes().update(Collections.singleton(change));
-        } else {
-          log.error("Test merge attempt for change: " + change.getId()
-              + " failed");
-        }
-      }
-    } catch (MergeException e) {
-      log.error("Test merge attempt for change: " + change.getId()
-          + " failed", e);
-    } catch (OrmException e) {
-      log.error("Test merge attempt for change: " + change.getId()
-          + " failed: Not able to query the database", e);
-    } catch (IOException e) {
-      log.error("Test merge attempt for change: " + change.getId()
-          + " failed", e);
-    } finally {
-      if (repo != null) {
-        repo.close();
-      }
-      if (db != null) {
-        db.close();
-      }
-    }
+    toUpdate = Lists.newArrayList();
   }
 
   private void setDestProject() throws MergeException {
@@ -262,15 +220,17 @@
     }
   }
 
-  public void merge() throws MergeException, NoSuchProjectException {
+  public void merge() throws MergeException {
     setDestProject();
     try {
       openSchema();
       openRepository();
-      openBranch();
+
+      RefUpdate branchUpdate = openBranch();
+      boolean reopen = false;
+
       final ListMultimap<SubmitType, Change> toSubmit =
           validateChangeList(db.changes().submitted(destBranch).toList());
-
       final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
           ArrayListMultimap.create();
       final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
@@ -280,10 +240,14 @@
         final Set<SubmitType> submitTypes =
             new HashSet<Project.SubmitType>(toMerge.keySet());
         for (final SubmitType submitType : submitTypes) {
-          final RefUpdate branchUpdate = openBranch();
+          if (reopen) {
+            branchUpdate = openBranch();
+          }
           final SubmitStrategy strategy = createStrategy(submitType);
           preMerge(strategy, toMerge.get(submitType));
           updateBranch(strategy, branchUpdate);
+          reopen = true;
+
           updateChangeStatus(toSubmit.get(submitType));
           updateSubscriptions(toSubmit.get(submitType));
 
@@ -308,6 +272,8 @@
         toMerge.putAll(toMergeNextTurn);
       }
 
+      updateChangeStatus(toUpdate);
+
       for (final CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
         final Capable capable = isSubmitStillPossible(commit);
         if (capable != Capable.OK) {
@@ -315,6 +281,11 @@
               message(commit.change, capable.getMessage()), false);
         }
       }
+    } catch (NoSuchProjectException noProject) {
+      log.warn(String.format(
+          "Project %s no longer exists, abandoning open changes",
+          destBranch.getParentKey().get()));
+      abandonAllOpenChanges();
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
     } finally {
@@ -388,13 +359,12 @@
         canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
   }
 
-  private void openRepository() throws MergeException {
+  private void openRepository() throws MergeException, NoSuchProjectException {
     final Project.NameKey name = destBranch.getParentKey();
     try {
       repo = repoManager.openRepository(name);
-    } catch (RepositoryNotFoundException notGit) {
-      final String m = "Repository \"" + name.get() + "\" unknown.";
-      throw new MergeException(m, notGit);
+    } catch (RepositoryNotFoundException notFound) {
+      throw new NoSuchProjectException(name, notFound);
     } catch (IOException err) {
       final String m = "Error opening repository \"" + name.get() + '"';
       throw new MergeException(m, err);
@@ -419,27 +389,15 @@
       if (branchUpdate.getOldObjectId() != null) {
         branchTip =
             (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
-      } else {
+      } else if (repo.getFullBranch().equals(destBranch.get())) {
         branchTip = null;
-      }
-
-      try {
-        final Ref destRef = repo.getRef(destBranch.get());
-        if (destRef != null) {
-          branchUpdate.setExpectedOldObjectId(destRef.getObjectId());
-        } else if (repo.getFullBranch().equals(destBranch.get())) {
-          branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
-        } else {
-          for (final Change c : db.changes().submitted(destBranch).toList()) {
-            setNew(c, message(c, "Your change could not be merged, "
-                + "because the destination branch does not exist anymore."));
-          }
+        branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+      } else {
+        for (final Change c : db.changes().submitted(destBranch).toList()) {
+          setNew(c, message(c, "Your change could not be merged, "
+              + "because the destination branch does not exist anymore."));
         }
-      } catch (IOException e) {
-        throw new MergeException(
-            "Failed to check existence of destination branch", e);
       }
-
       return branchUpdate;
     } catch (IOException e) {
       throw new MergeException("Cannot open branch", e);
@@ -455,7 +413,7 @@
     }
 
     try {
-      for (final Ref r : repo.getAllRefs().values()) {
+      for (final Ref r : repo.getRefDatabase().getRefs(ALL).values()) {
         if (r.getName().startsWith(Constants.R_HEADS)) {
           try {
             alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
@@ -476,8 +434,15 @@
     final ListMultimap<SubmitType, Change> toSubmit =
         ArrayListMultimap.create();
 
+    final Map<String, Ref> allRefs;
+    try {
+      allRefs = repo.getRefDatabase().getRefs(ALL);
+    } catch (IOException e) {
+      throw new MergeException(e.getMessage(), e);
+    }
+
     final Set<ObjectId> tips = new HashSet<ObjectId>();
-    for (final Ref r : repo.getAllRefs().values()) {
+    for (final Ref r : allRefs.values()) {
       tips.add(r.getObjectId());
     }
 
@@ -487,6 +452,7 @@
       if (chg.currentPatchSetId() == null) {
         commits.put(changeId, CodeReviewCommit
             .error(CommitMergeStatus.NO_PATCH_SET));
+        toUpdate.add(chg);
         continue;
       }
 
@@ -500,6 +466,7 @@
           || ps.getRevision().get() == null) {
         commits.put(changeId, CodeReviewCommit
             .error(CommitMergeStatus.NO_PATCH_SET));
+        toUpdate.add(chg);
         continue;
       }
 
@@ -510,6 +477,7 @@
       } catch (IllegalArgumentException iae) {
         commits.put(changeId, CodeReviewCommit
             .error(CommitMergeStatus.NO_PATCH_SET));
+        toUpdate.add(chg);
         continue;
       }
 
@@ -525,6 +493,7 @@
         //
         commits.put(changeId, CodeReviewCommit
             .error(CommitMergeStatus.REVISION_GONE));
+        toUpdate.add(chg);
         continue;
       }
 
@@ -535,53 +504,17 @@
         log.error("Invalid commit " + id.name() + " on " + chg.getKey(), e);
         commits.put(changeId, CodeReviewCommit
             .error(CommitMergeStatus.REVISION_GONE));
+        toUpdate.add(chg);
         continue;
       }
 
-      if (GitRepositoryManager.REF_CONFIG.equals(destBranch.get())) {
-        final Project.NameKey newParent;
-        try {
-          ProjectConfig cfg =
-              new ProjectConfig(destProject.getProject().getNameKey());
-          cfg.load(repo, commit);
-          newParent = cfg.getProject().getParent(allProjectsName);
-        } catch (Exception e) {
-          commits.put(changeId, CodeReviewCommit
-              .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION));
-          continue;
-        }
-        final Project.NameKey oldParent =
-            destProject.getProject().getParent(allProjectsName);
-        if (oldParent == null) {
-          // update of the 'All-Projects' project
-          if (newParent != null) {
-            commits.put(changeId, CodeReviewCommit
-                .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT));
-            continue;
-          }
-        } else {
-          if (!oldParent.equals(newParent)) {
-            final PatchSetApproval psa = getSubmitter(db, ps.getId());
-            if (psa == null) {
-              commits.put(changeId, CodeReviewCommit
-                  .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
-              continue;
-            }
-            final IdentifiedUser submitter =
-                identifiedUserFactory.create(psa.getAccountId());
-            if (!submitter.getCapabilities().canAdministrateServer()) {
-              commits.put(changeId, CodeReviewCommit
-                  .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
-              continue;
-            }
-
-            if (projectCache.get(newParent) == null) {
-              commits.put(changeId, CodeReviewCommit
-                  .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND));
-              continue;
-            }
-          }
-        }
+      MergeValidators mergeValidators = mergeValidatorsFactory.create();
+      try {
+        mergeValidators.validatePreMerge(repo, commit, destProject, destBranch, ps.getId());
+      } catch (MergeValidationException mve) {
+        commits.put(changeId, CodeReviewCommit.error(mve.getStatus()));
+        toUpdate.add(chg);
+        continue;
       }
 
       commit.change = chg;
@@ -613,6 +546,7 @@
       if (submitType == null) {
         commits.put(changeId,
             CodeReviewCommit.error(CommitMergeStatus.NO_SUBMIT_TYPE));
+        toUpdate.add(chg);
         continue;
       }
 
@@ -761,6 +695,8 @@
         }
       } catch (OrmException err) {
         log.warn("Error updating change status for " + c.getId(), err);
+      } catch (IOException err) {
+        log.warn("Error updating change status for " + c.getId(), err);
       }
     }
   }
@@ -784,7 +720,7 @@
     final Capable capable;
     final Change c = commit.change;
     final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit);
-    final long now = System.currentTimeMillis();
+    final long now = TimeUtil.nowMs();
     final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
     if (submitStillPossible && now < waitUntil) {
       // If we waited a short while we might still be able to get
@@ -870,15 +806,14 @@
     } catch (OrmException e) {
       return null;
     }
-    final ChangeMessage m =
-        new ChangeMessage(new ChangeMessage.Key(c.getId(), uuid), null,
-            c.currentPatchSetId());
+    ChangeMessage m = new ChangeMessage(new ChangeMessage.Key(c.getId(), uuid),
+        null, TimeUtil.nowTs(), c.currentPatchSetId());
     m.setMessage(body);
     return m;
   }
 
   private void setMerged(final Change c, final ChangeMessage msg)
-      throws OrmException {
+      throws OrmException, IOException {
     try {
       db.changes().beginTransaction(c.getId());
 
@@ -897,7 +832,7 @@
         try {
           hooks.doChangeMergedHook(c,
               accountCache.get(submitter.getAccountId()).getAccount(),
-              db.patchSets().get(c.currentPatchSetId()), db);
+              db.patchSets().get(commit.patchsetId), db);
         } catch (OrmException ex) {
           log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
         }
@@ -905,6 +840,7 @@
     } finally {
       db.rollback();
     }
+    indexer.index(c);
   }
 
   private void setMergedPatchSet(Change.Id changeId, final PatchSet.Id merged)
@@ -1026,61 +962,41 @@
     sendMergeFail(c, msg, true);
   }
 
-  private boolean isDuplicate(ChangeMessage msg) {
+  private enum RetryStatus {
+    UNSUBMIT, RETRY_NO_MESSAGE, RETRY_ADD_MESSAGE;
+  }
+
+  private RetryStatus getRetryStatus(
+      @Nullable PatchSetApproval submitter,
+      ChangeMessage msg) {
+    if (submitter != null
+        && TimeUtil.nowMs() - submitter.getGranted().getTime()
+          > MAX_SUBMIT_WINDOW) {
+      return RetryStatus.UNSUBMIT;
+    }
+
     try {
       ChangeMessage last = Iterables.getLast(db.changeMessages().byChange(
           msg.getPatchSetId().getParentKey()), null);
       if (last != null) {
-        long lastMs = last.getWrittenOn().getTime();
-        long msgMs = msg.getWrittenOn().getTime();
         if (Objects.equal(last.getAuthor(), msg.getAuthor())
-            && Objects.equal(last.getMessage(), msg.getMessage())
-            && msgMs - lastMs < DUPLICATE_MESSAGE_INTERVAL) {
-          return true;
+            && Objects.equal(last.getMessage(), msg.getMessage())) {
+          long lastMs = last.getWrittenOn().getTime();
+          long msgMs = msg.getWrittenOn().getTime();
+          return msgMs - lastMs > MAX_SUBMIT_WINDOW
+              ? RetryStatus.UNSUBMIT
+              : RetryStatus.RETRY_NO_MESSAGE;
         }
       }
+      return RetryStatus.RETRY_ADD_MESSAGE;
     } catch (OrmException err) {
-      log.warn("Cannot check previous merge failure message", err);
+      log.warn("Cannot check previous merge failure, unsubmitting", err);
+      return RetryStatus.UNSUBMIT;
     }
-    return false;
   }
 
   private void sendMergeFail(final Change c, final ChangeMessage msg,
-      final boolean makeNew) {
-    if (makeNew) {
-      try {
-        db.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change c) {
-            if (c.getStatus().isOpen()) {
-              c.setStatus(Change.Status.NEW);
-              ChangeUtil.updated(c);
-            }
-            return c;
-          }
-        });
-      } catch (OrmConcurrencyException err) {
-      } catch (OrmException err) {
-        log.warn("Cannot update change status", err);
-      }
-    } else {
-      try {
-        ChangeUtil.touch(c, db);
-      } catch (OrmException err) {
-        log.warn("Cannot update change timestamp", err);
-      }
-    }
-
-    if (isDuplicate(msg)) {
-      return;
-    }
-
-    try {
-      db.changeMessages().insert(Collections.singleton(msg));
-    } catch (OrmException err) {
-      log.warn("Cannot record merge failure message", err);
-    }
-
+      boolean makeNew) {
     PatchSetApproval submitter = null;
     try {
       submitter = getSubmitter(db, c.currentPatchSetId());
@@ -1088,6 +1004,49 @@
       log.error("Cannot get submitter", e);
     }
 
+    if (!makeNew) {
+      RetryStatus retryStatus = getRetryStatus(submitter, msg);
+      if (retryStatus == RetryStatus.RETRY_NO_MESSAGE) {
+        return;
+      } else if (retryStatus == RetryStatus.UNSUBMIT) {
+        makeNew = true;
+      }
+    }
+
+    final boolean setStatusNew = makeNew;
+    Change change = null;
+    try {
+      db.changes().beginTransaction(c.getId());
+      try {
+        change = db.changes().atomicUpdate(
+            c.getId(),
+            new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change c) {
+            if (c.getStatus().isOpen()) {
+              if (setStatusNew) {
+                c.setStatus(Change.Status.NEW);
+              }
+              ChangeUtil.updated(c);
+            }
+            return c;
+          }
+        });
+        db.changeMessages().insert(Collections.singleton(msg));
+        db.commit();
+      } finally {
+        db.rollback();
+      }
+    } catch (OrmException err) {
+      log.warn("Cannot record merge failure message", err);
+    }
+
+    CheckedFuture<?, IOException> indexFuture;
+    if (change != null) {
+      indexFuture = indexer.indexAsync(change);
+    } else {
+      indexFuture = null;
+    }
     final PatchSetApproval from = submitter;
     workQueue.getDefaultQueue()
         .submit(requestScopePropagator.wrap(new Runnable() {
@@ -1134,5 +1093,68 @@
         log.error("Cannot run hook for merge failed " + c.getId(), ex);
       }
     }
+    if (indexFuture != null) {
+      try {
+        indexFuture.checkedGet();
+      } catch (IOException e) {
+        log.error("Failed to index new change message", e);
+      }
+    }
+  }
+
+  private void abandonAllOpenChanges() {
+    Exception err = null;
+    try {
+      openSchema();
+      for (Change c : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
+        abandonOneChange(c);
+      }
+      db.close();
+      db = null;
+    } catch (IOException e) {
+      err = e;
+    } catch (OrmException e) {
+      err = e;
+    }
+    if (err != null) {
+      log.warn(String.format(
+          "Cannot abandon changes for deleted project %s",
+          destBranch.getParentKey().get()), err);
+    }
+  }
+
+  private void abandonOneChange(Change change) throws OrmException,
+      IOException {
+    db.changes().beginTransaction(change.getId());
+    try {
+      change = db.changes().atomicUpdate(
+        change.getId(),
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus().isOpen()) {
+              change.setStatus(Change.Status.ABANDONED);
+              return change;
+            }
+            return null;
+          }
+        });
+      if (change != null) {
+        ChangeMessage msg = new ChangeMessage(
+            new ChangeMessage.Key(
+                change.getId(),
+                ChangeUtil.messageUUID(db)),
+            null,
+            change.getLastUpdatedOn(),
+            change.currentPatchSetId());
+        msg.setMessage("Project was deleted.");
+        db.changeMessages().insert(Collections.singleton(msg));
+        new ApprovalsUtil(db).syncChangeStatus(change);
+        db.commit();
+        indexer.index(change);
+      }
+    } finally {
+      db.rollback();
+    }
   }
 }
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 862eb2b..9a407e1 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -38,7 +39,6 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -69,8 +69,6 @@
 import java.util.Set;
 import java.util.TimeZone;
 
-import javax.annotation.Nullable;
-
 public class MergeUtil {
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
 
@@ -180,8 +178,8 @@
     return submitter;
   }
 
-  public CodeReviewCommit createCherryPickFromCommit(Repository repo,
-      ObjectInserter inserter, CodeReviewCommit mergeTip, CodeReviewCommit originalCommit,
+  public RevCommit createCherryPickFromCommit(Repository repo,
+      ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
       PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
       throws MissingObjectException, IncorrectObjectTypeException, IOException {
 
@@ -189,20 +187,18 @@
 
     m.setBase(originalCommit.getParent(0));
     if (m.merge(mergeTip, originalCommit)) {
+      ObjectId tree = m.getResultTreeId();
+      if (tree.equals(mergeTip.getTree())) {
+        return null;
+      }
 
-      final CommitBuilder mergeCommit = new CommitBuilder();
-
-      mergeCommit.setTreeId(m.getResultTreeId());
+      CommitBuilder mergeCommit = new CommitBuilder();
+      mergeCommit.setTreeId(tree);
       mergeCommit.setParentId(mergeTip);
       mergeCommit.setAuthor(originalCommit.getAuthorIdent());
       mergeCommit.setCommitter(cherryPickCommitterIdent);
       mergeCommit.setMessage(commitMsg);
-
-      final ObjectId id = commit(inserter, mergeCommit);
-      final CodeReviewCommit newCommit =
-          (CodeReviewCommit) rw.parseCommit(id);
-
-      return newCommit;
+      return rw.parseCommit(commit(inserter, mergeCommit));
     } else {
       return null;
     }
@@ -489,15 +485,10 @@
 
   public ObjectInserter createDryRunInserter() {
     return new ObjectInserter() {
-      private final MutableObjectId buf = new MutableObjectId();
-      private final static int LAST_BYTE = Constants.OBJECT_ID_LENGTH - 1;
-
       @Override
       public ObjectId insert(int objectType, long length, InputStream in)
           throws IOException {
-        // create non-existing dummy ID
-        buf.setByte(LAST_BYTE, buf.getByte(LAST_BYTE) + 1);
-        return buf.copy();
+        return idFor(objectType, length, in);
       }
 
       @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index dc08a6c..43e0975 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -35,30 +36,34 @@
     private final InternalFactory factory;
     private final GitRepositoryManager mgr;
     private final PersonIdent serverIdent;
-    private final PersonIdent userIdent;
+    private final Provider<IdentifiedUser> identifiedUser;
 
     @Inject
     User(InternalFactory factory, GitRepositoryManager mgr,
-        @GerritPersonIdent PersonIdent serverIdent, IdentifiedUser currentUser) {
+        @GerritPersonIdent PersonIdent serverIdent,
+        Provider<IdentifiedUser> identifiedUser) {
       this.factory = factory;
       this.mgr = mgr;
       this.serverIdent = serverIdent;
-      this.userIdent = currentUser.newCommitterIdent( //
-          serverIdent.getWhen(), //
-          serverIdent.getTimeZone());
+      this.identifiedUser = identifiedUser;
     }
 
     public PersonIdent getUserPersonIdent() {
-      return userIdent;
+      return createPersonIdent();
     }
 
     public MetaDataUpdate create(Project.NameKey name)
         throws RepositoryNotFoundException, IOException {
       MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
-      md.getCommitBuilder().setAuthor(userIdent);
+      md.getCommitBuilder().setAuthor(createPersonIdent());
       md.getCommitBuilder().setCommitter(serverIdent);
       return md;
     }
+
+    private PersonIdent createPersonIdent() {
+      return identifiedUser.get().newCommitterIdent(
+          serverIdent.getWhen(), serverIdent.getTimeZone());
+    }
   }
 
   public static class Server {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 9c5633b..f7b5ab0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -16,6 +16,8 @@
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import com.google.common.base.Strings;
+
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.slf4j.Logger;
@@ -62,7 +64,7 @@
   public class Task implements ProgressMonitor {
     private final String name;
     private final int total;
-    private volatile int count;
+    private int count;
     private int lastPercent;
 
     Task(final String subTaskName, final int totalWork) {
@@ -73,29 +75,35 @@
     /**
      * Indicate that work has been completed on this sub-task.
      * <p>
-     * Must be called from the worker thread.
+     * Must be called from a worker thread.
      *
      * @param completed number of work units completed.
      */
     @Override
     public void update(final int completed) {
-      count += completed;
-      if (total != UNKNOWN) {
-        int percent = count * 100 / total;
-        if (percent > lastPercent) {
-          lastPercent = percent;
-          wakeUp();
+      boolean w = false;
+      synchronized (this) {
+        count += completed;
+        if (total != UNKNOWN) {
+          int percent = count * 100 / total;
+          if (percent > lastPercent) {
+            lastPercent = percent;
+            w = true;
+          }
         }
       }
+      if (w) {
+        wakeUp();
+      }
     }
 
     /**
      * Indicate that this sub-task is finished.
      * <p>
-     * Must be called from the worker thread.
+     * Must be called from a worker thread.
      */
     public void end() {
-      if (total == UNKNOWN && count > 0) {
+      if (total == UNKNOWN && getCount() > 0) {
         wakeUp();
       }
     }
@@ -116,6 +124,10 @@
     public boolean isCancelled() {
       return false;
     }
+
+    public synchronized int getCount() {
+      return count;
+    }
   }
 
   private final OutputStream out;
@@ -165,19 +177,19 @@
   /**
    * Wait for a task managed by a {@link Future}.
    * <p>
-   * Must be called from the main thread, <em>not</em> the worker thread. Once
-   * the worker thread calls {@link #end()}, the future has an additional
+   * Must be called from the main thread, <em>not</em> a worker thread. Once a
+   * worker thread calls {@link #end()}, the future has an additional
    * <code>maxInterval</code> to finish before it is forcefully cancelled and
    * {@link ExecutionException} is thrown.
    *
-   * @param workerFuture a future that returns when the worker thread is
-   *     finished.
+   * @param workerFuture a future that returns when worker threads are finished.
    * @param timeoutTime overall timeout for the task; the future is forcefully
    *     cancelled if the task exceeds the timeout. Non-positive values indicate
    *     no timeout.
    * @param timeoutUnit unit for overall task timeout.
-   * @throws ExecutionException if this thread or the worker thread was
-   *     interrupted, the worker was cancelled, or the worker timed out.
+   * @throws ExecutionException if this thread or a worker thread was
+   *     interrupted, the worker was cancelled, or timed out waiting for a
+   *     worker to call {@link #end()}.
    */
   public void waitFor(final Future<?> workerFuture, final long timeoutTime,
       final TimeUnit timeoutUnit) throws ExecutionException {
@@ -268,7 +280,7 @@
   /**
    * End the overall task.
    * <p>
-   * Must be called from the worker thread.
+   * Must be called from a worker thread.
    */
   public synchronized void end() {
     done = true;
@@ -319,7 +331,10 @@
           first = false;
         }
 
-        s.append(' ').append(t.name).append(": ");
+        s.append(' ');
+        if (!Strings.isNullOrEmpty(t.name)) {
+          s.append(t.name).append(": ");
+        }
         if (t.total == UNKNOWN) {
           s.append(count);
         } else {
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 bfaa97e..e4ee1bf 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
@@ -46,6 +46,7 @@
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.project.CommentLinkInfo;
 
@@ -59,6 +60,7 @@
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -66,6 +68,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
@@ -107,6 +110,7 @@
   private static final String RECEIVE = "receive";
   private static final String KEY_REQUIRE_SIGNED_OFF_BY = "requireSignedOffBy";
   private static final String KEY_REQUIRE_CHANGE_ID = "requireChangeId";
+  private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";
   private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
       "requireContributorAgreement";
 
@@ -124,10 +128,15 @@
   private static final String KEY_FUNCTION = "function";
   private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
   private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
+  private static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
+  private static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoCodeChange";
   private static final String KEY_VALUE = "value";
   private static final String KEY_CAN_OVERRIDE = "canOverride";
+  private static final String KEY_Branch = "branch";
   private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
-      "MaxWithBlock", "MaxNoBlock", "NoBlock", "NoOp");
+      "MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp");
+
+  private static final String PLUGIN = "plugin";
 
   private static final SubmitType defaultSubmitAction =
       SubmitType.MERGE_IF_NECESSARY;
@@ -145,6 +154,8 @@
   private List<CommentLinkInfo> commentLinkSections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
+  private long maxObjectSizeLimit;
+  private Map<String, Config> pluginConfigs;
 
   public static ProjectConfig read(MetaDataUpdate update) throws IOException,
       ConfigInvalidException {
@@ -319,6 +330,14 @@
   }
 
   /**
+   * @return the maxObjectSizeLimit for this project, if set. Zero if this
+   *         project doesn't define own maxObjectSizeLimit.
+   */
+  public long getMaxObjectSizeLimit() {
+    return maxObjectSizeLimit;
+  }
+
+  /**
    * Check all GroupReferences use current group name, repairing stale ones.
    *
    * @param groupBackend cache to use when looking up group information by UUID.
@@ -372,6 +391,7 @@
     p.setUseContributorAgreements(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, Project.InheritableBoolean.INHERIT));
     p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, Project.InheritableBoolean.INHERIT));
     p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, Project.InheritableBoolean.INHERIT));
+    p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
     p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, Project.InheritableBoolean.INHERIT));
@@ -386,6 +406,9 @@
     loadNotifySections(rc, groupsByName);
     loadLabelSections(rc);
     loadCommentLinkSections(rc);
+    loadPluginSections(rc);
+
+    maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0);
   }
 
   private void loadAccountsSection(
@@ -512,7 +535,7 @@
           if (isPermission(varName)) {
             Permission perm = as.getPermission(varName, true);
             loadPermissionRules(rc, ACCESS, refName, varName, groupsByName,
-                perm, perm.isLabel());
+                perm, Permission.hasRange(varName));
           }
         }
       }
@@ -520,15 +543,13 @@
 
     AccessSection capability = null;
     for (String varName : rc.getNames(CAPABILITY)) {
-      if (GlobalCapability.isCapability(varName)) {
-        if (capability == null) {
-          capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
-          accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
-        }
-        Permission perm = capability.getPermission(varName, true);
-        loadPermissionRules(rc, CAPABILITY, null, varName, groupsByName, perm,
-            GlobalCapability.hasRange(varName));
+      if (capability == null) {
+        capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
+        accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
       }
+      Permission perm = capability.getPermission(varName, true);
+      loadPermissionRules(rc, CAPABILITY, null, varName, groupsByName, perm,
+          GlobalCapability.hasRange(varName));
     }
   }
 
@@ -638,12 +659,23 @@
           rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, false));
       label.setCopyMaxScore(
           rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE, false));
+      label.setCopyAllScoresOnTrivialRebase(
+          rc.getBoolean(LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE, false));
+      label.setCopyAllScoresIfNoCodeChange(
+          rc.getBoolean(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE, false));
       label.setCanOverride(
           rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, true));
+      label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_Branch));
       labelSections.put(name, label);
     }
   }
 
+  private List<String> getStringListOrNull(Config rc, String section,
+      String subSection, String name) {
+    String[] ac = rc.getStringList(section, subSection, name);
+    return ac.length == 0 ? null : Arrays.asList(ac);
+  }
+
   private void loadCommentLinkSections(Config rc) {
     Set<String> subsections = rc.getSubsections(COMMENTLINK);
     commentLinkSections = Lists.newArrayListWithCapacity(subsections.size());
@@ -663,6 +695,27 @@
     commentLinkSections = ImmutableList.copyOf(commentLinkSections);
   }
 
+  private void loadPluginSections(Config rc) {
+    pluginConfigs = Maps.newHashMap();
+    for (String plugin : rc.getSubsections(PLUGIN)) {
+      Config pluginConfig = new Config();
+      pluginConfigs.put(plugin, pluginConfig);
+      for (String name : rc.getNames(PLUGIN, plugin)) {
+        pluginConfig.setStringList(PLUGIN, plugin, name,
+            Arrays.asList(rc.getStringList(PLUGIN, plugin, name)));
+      }
+    }
+  }
+
+  public PluginConfig getPluginConfig(String pluginName) {
+    Config pluginConfig = pluginConfigs.get(pluginName);
+    if (pluginConfig == null) {
+      pluginConfig = new Config();
+      pluginConfigs.put(pluginName, pluginConfig);
+    }
+    return new PluginConfig(pluginName, pluginConfig, this);
+  }
+
   private Map<String, GroupReference> readGroupList() throws IOException {
     groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
     Map<String, GroupReference> groupsByName =
@@ -711,11 +764,12 @@
     set(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.getUseContributorAgreements(), Project.InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.getUseSignedOffBy(), Project.InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), Project.InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
     set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), Project.InheritableBoolean.INHERIT);
 
-    set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
+    set(rc, PROJECT, null, KEY_STATE, p.getState(), defaultStateValue);
 
     set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard());
     set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard());
@@ -727,11 +781,41 @@
     saveNotifySections(rc, keepGroups);
     groupsByUUID.keySet().retainAll(keepGroups);
     saveLabelSections(rc);
+    savePluginSections(rc);
 
     saveConfig(PROJECT_CONFIG, rc);
     saveGroupList();
   }
 
+  public static final String validMaxObjectSizeLimit(String value)
+      throws ConfigInvalidException {
+    if (value == null) {
+      return null;
+    }
+    value = value.trim();
+    if (value.isEmpty()) {
+      return null;
+    }
+    Config cfg = new Config();
+    cfg.fromText("[s]\nn=" + value);
+    try {
+      long s = cfg.getLong("s", "n", 0);
+      if (s < 0) {
+        throw new ConfigInvalidException(String.format(
+            "Negative value '%s' not allowed as %s", value,
+            KEY_MAX_OBJECT_SIZE_LIMIT));
+      }
+      if (s == 0) {
+        // return null for the default so that it is not persisted
+        return null;
+      }
+      return value;
+    } catch (IllegalArgumentException e) {
+      throw new ConfigInvalidException(
+          String.format("Value '%s' not parseable as a Long", value), e);
+    }
+  }
+
   private void saveAccountsSection(Config rc, Set<AccountGroup.UUID> keepGroups) {
     if (accountsSection != null) {
       rc.setStringList(ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY,
@@ -836,8 +920,7 @@
         rc.setStringList(CAPABILITY, null, permission.getName(), rules);
       }
       for (String varName : rc.getNames(CAPABILITY)) {
-        if (GlobalCapability.isCapability(varName)
-            && !have.contains(varName.toLowerCase())) {
+        if (!have.contains(varName.toLowerCase())) {
           rc.unset(CAPABILITY, null, varName);
         }
       }
@@ -870,7 +953,7 @@
       for (Permission permission : sort(as.getPermissions())) {
         have.add(permission.getName().toLowerCase());
 
-        boolean needRange = permission.isLabel();
+        boolean needRange = Permission.hasRange(permission.getName());
         List<String> rules = new ArrayList<String>();
         for (PermissionRule rule : sort(permission.getRules())) {
           GroupReference group = rule.getGroup();
@@ -929,6 +1012,16 @@
       } else {
         rc.unset(LABEL, name, KEY_COPY_MAX_SCORE);
       }
+      if (label.isCopyAllScoresOnTrivialRebase()) {
+        rc.setBoolean(LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE, true);
+      } else {
+        rc.unset(LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
+      }
+      if (label.isCopyAllScoresIfNoCodeChange()) {
+        rc.setBoolean(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE, true);
+      } else {
+        rc.unset(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE);
+      }
       if (!label.canOverride()) {
         rc.setBoolean(LABEL, name, KEY_CAN_OVERRIDE, false);
       } else {
@@ -948,6 +1041,22 @@
     }
   }
 
+  private void savePluginSections(Config rc) {
+    List<String> existing = Lists.newArrayList(rc.getSubsections(PLUGIN));
+    for (String name : existing) {
+      rc.unsetSection(PLUGIN, name);
+    }
+
+    for (Entry<String, Config> e : pluginConfigs.entrySet()) {
+      String plugin = e.getKey();
+      Config pluginConfig = e.getValue();
+      for (String name : pluginConfig.getNames(PLUGIN, plugin)) {
+        rc.setStringList(PLUGIN, plugin, name,
+            Arrays.asList(pluginConfig.getStringList(PLUGIN, plugin, name)));
+      }
+    }
+  }
+
   private void saveGroupList() throws IOException {
     if (groupsByUUID.isEmpty()) {
       saveFile(GROUP_LIST, null);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
index 8490ea1..8fec2fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
+
 import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.changedetail.PathConflictException;
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -25,6 +28,7 @@
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -35,12 +39,14 @@
 
   private final RebaseChange rebaseChange;
   private final Map<Change.Id, CodeReviewCommit> newCommits;
+  private final PersonIdent committerIdent;
 
   RebaseIfNecessary(final SubmitStrategy.Arguments args,
-      final RebaseChange rebaseChange) {
+      final RebaseChange rebaseChange, PersonIdent committerIdent) {
     super(args);
     this.rebaseChange = rebaseChange;
     this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
+    this.committerIdent = committerIdent;
   }
 
   @Override
@@ -73,11 +79,14 @@
 
         } else {
           try {
+            final IdentifiedUser uploader =
+                args.identifiedUserFactory.create(
+                    args.mergeUtil.getSubmitter(n.patchsetId).getAccountId());
             final PatchSet newPatchSet =
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
-                    n.patchsetId, n.change,
-                    args.mergeUtil.getSubmitter(n.patchsetId).getAccountId(),
-                    newMergeTip, args.mergeUtil);
+                    n.patchsetId, n.change, uploader,
+                    newMergeTip, args.mergeUtil, committerIdent,
+                    false, false, ValidatePolicy.NONE);
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.mergeUtil.getApprovalsForCommit(n)) {
               approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
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 3e28b07..06e88e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.git;
 
-import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
@@ -45,6 +46,7 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.PermissionRule;
@@ -62,9 +64,12 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.PatchSetInserter.ChangeKind;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -76,6 +81,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.MergedSender;
@@ -89,6 +95,7 @@
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -121,6 +128,7 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 import org.eclipse.jgit.transport.UploadPack;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
@@ -129,7 +137,6 @@
 
 import java.io.IOException;
 import java.io.StringWriter;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -143,8 +150,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import javax.annotation.Nullable;
-
 /** Receives change upload using the Git receive-pack protocol. */
 public class ReceiveCommits {
   private static final Logger log =
@@ -261,10 +266,12 @@
   private final CommitValidators.Factory commitValidatorsFactory;
   private final TrackingFooters trackingFooters;
   private final TagCache tagCache;
-  private final ChangeInserter changeInserter;
+  private final AccountCache accountCache;
+  private final ChangeInserter.Factory changeInserterFactory;
   private final WorkQueue workQueue;
   private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
+  private final ChangeIndexer indexer;
   private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
@@ -291,6 +298,7 @@
   private final SubmoduleOp.Factory subOpFactory;
   private final Provider<Submit> submitProvider;
   private final MergeQueue mergeQueue;
+  private final MergeUtil.Factory mergeUtilFactory;
 
   private final List<CommitValidationMessage> messages = new ArrayList<CommitValidationMessage>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
@@ -316,8 +324,9 @@
       final ProjectCache projectCache,
       final GitRepositoryManager repoManager,
       final TagCache tagCache,
+      final AccountCache accountCache,
       final ChangeCache changeCache,
-      final ChangeInserter changeInserter,
+      final ChangeInserter.Factory changeInserterFactory,
       final CommitValidators.Factory commitValidatorsFactory,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @GerritPersonIdent final PersonIdent gerritIdent,
@@ -325,6 +334,7 @@
       final WorkQueue workQueue,
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
+      final ChangeIndexer indexer,
       final SshInfo sshInfo,
       final AllProjectsName allProjectsName,
       ReceiveConfig config,
@@ -332,7 +342,8 @@
       @Assisted final Repository repo,
       final SubmoduleOp.Factory subOpFactory,
       final Provider<Submit> submitProvider,
-      final MergeQueue mergeQueue) throws IOException {
+      final MergeQueue mergeQueue,
+      final MergeUtil.Factory mergeUtilFactory) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
     this.schemaFactory = schemaFactory;
@@ -350,11 +361,13 @@
     this.canonicalWebUrl = canonicalWebUrl;
     this.trackingFooters = trackingFooters;
     this.tagCache = tagCache;
-    this.changeInserter = changeInserter;
+    this.accountCache = accountCache;
+    this.changeInserterFactory = changeInserterFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.workQueue = workQueue;
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
+    this.indexer = indexer;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
@@ -369,6 +382,7 @@
     this.subOpFactory = subOpFactory;
     this.submitProvider = submitProvider;
     this.mergeQueue = mergeQueue;
+    this.mergeUtilFactory = mergeUtilFactory;
 
     this.messageSender = new ReceivePackMessageSender();
 
@@ -384,10 +398,18 @@
     List<AdvertiseRefsHook> advHooks = new ArrayList<AdvertiseRefsHook>(3);
     advHooks.add(new AdvertiseRefsHook() {
       @Override
-      public void advertiseRefs(BaseReceivePack rp) {
+      public void advertiseRefs(BaseReceivePack rp)
+          throws ServiceMayNotContinueException {
         allRefs = rp.getAdvertisedRefs();
         if (allRefs == null) {
-          allRefs = rp.getRepository().getAllRefs();
+          try {
+            allRefs = rp.getRepository().getRefDatabase().getRefs(ALL);
+          } catch (IOException e) {
+            ServiceMayNotContinueException ex =
+                new ServiceMayNotContinueException(e.getMessage());
+            ex.initCause(e);
+            throw ex;
+          }
         }
         rp.setAdvertisedRefs(allRefs, rp.getAdvertisedObjects());
       }
@@ -397,7 +419,8 @@
       }
     });
     advHooks.add(rp.getAdvertiseRefsHook());
-    advHooks.add(new ReceiveCommitsAdvertiseRefsHook());
+    advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
+        db, projectControl.getProject().getNameKey()));
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
   }
 
@@ -432,80 +455,6 @@
     return rp;
   }
 
-  /** Scan part of history and include it in the advertisement. */
-  public void advertiseHistory() {
-    Set<ObjectId> toInclude = new HashSet<ObjectId>();
-
-    // Advertise some recent open changes, in case a commit is based one.
-    try {
-      Set<PatchSet.Id> toGet = new HashSet<PatchSet.Id>();
-      for (Change change : db.changes()
-          .byProjectOpenNext(project.getNameKey(), "z", 32)) {
-        PatchSet.Id id = change.currentPatchSetId();
-        if (id != null) {
-          toGet.add(id);
-        }
-      }
-      for (PatchSet ps : db.patchSets().get(toGet)) {
-        if (ps.getRevision() != null && ps.getRevision().get() != null) {
-          toInclude.add(ObjectId.fromString(ps.getRevision().get()));
-        }
-      }
-    } catch (OrmException err) {
-      log.error("Cannot list open changes of " + project.getNameKey(), err);
-    }
-
-    // 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 = rp.getAdvertisedObjects();
-    RevWalk rw = rp.getRevWalk();
-    for (ObjectId haveId : alreadySending) {
-      try {
-        rw.markStart(rw.parseCommit(haveId));
-      } catch (IOException badCommit) {
-        continue;
-      }
-    }
-
-    int stepCnt = 0;
-    RevCommit c;
-    try {
-      while ((c = rw.next()) != null && toInclude.size() < max) {
-        if (alreadySending.contains(c)) {
-        } else if (toInclude.contains(c)) {
-        } else if (c.getParentCount() > 1) {
-        } else if (toInclude.size() < base) {
-          toInclude.add(c);
-        } else {
-          stepCnt = ++stepCnt % step;
-          if (stepCnt == 0) {
-            toInclude.add(c);
-          }
-        }
-      }
-    } catch (IOException err) {
-      log.error("Error trying to advertise history on " + project.getNameKey(), err);
-    }
-    rw.reset();
-    rp.getAdvertisedObjects().addAll(toInclude);
-  }
-
   /** Determine if the user can upload commits. */
   public Capable canUpload() {
     Capable result = projectControl.canPushToAtLeastOneRef();
@@ -1143,6 +1092,10 @@
 
     magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
     magicBranch.ctl = projectControl.controlForRef(ref);
+    if (!magicBranch.ctl.canWrite()) {
+      reject(cmd, "project is read only");
+      return;
+    }
     if (!magicBranch.ctl.canUpload()) {
       errors.put(Error.CODE_REVIEW, ref);
       reject(cmd, "cannot upload review");
@@ -1315,6 +1268,11 @@
       walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
       if (magicBranch.baseCommit != null) {
         walk.markUninteresting(magicBranch.baseCommit);
+        assert magicBranch.ctl != null;
+        Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
+        if (targetRef != null) {
+          walk.markUninteresting(walk.parseCommit(targetRef.getObjectId()));
+        }
       } else {
         markHeadsAsUninteresting(
             walk,
@@ -1344,7 +1302,7 @@
         Change.Key changeKey = new Change.Key("I" + c.name());
         final List<String> idList = c.getFooterLines(CHANGE_ID);
         if (idList.isEmpty()) {
-          newChanges.add(new CreateRequest(c, changeKey));
+          newChanges.add(new CreateRequest(magicBranch.ctl, c, changeKey));
           continue;
         }
 
@@ -1394,7 +1352,7 @@
 
           newChangeIds.add(p.changeKey);
         }
-        newChanges.add(new CreateRequest(p.commit, p.changeKey));
+        newChanges.add(new CreateRequest(magicBranch.ctl, p.commit, p.changeKey));
       }
     } catch (IOException e) {
       // Should never happen, the core receive process would have
@@ -1460,34 +1418,23 @@
   private class CreateRequest {
     final RevCommit commit;
     final Change change;
-    final PatchSet ps;
     final ReceiveCommand cmd;
-    private final PatchSetInfo info;
+    final ChangeInserter ins;
     boolean created;
 
-    CreateRequest(RevCommit c, Change.Key changeKey) throws OrmException {
+    CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
+        throws OrmException {
       commit = c;
-
       change = new Change(changeKey,
           new Change.Id(db.nextChangeId()),
           currentUser.getAccountId(),
-          magicBranch.dest);
+          magicBranch.dest,
+          TimeUtil.nowTs());
       change.setTopic(magicBranch.topic);
-
-      ps = new PatchSet(new PatchSet.Id(change.getId(), INITIAL_PATCH_SET_ID));
-      ps.setCreatedOn(change.getCreatedOn());
-      ps.setUploader(change.getOwner());
-      ps.setRevision(toRevId(c));
-
-      if (magicBranch.isDraft()) {
-        change.setStatus(Change.Status.DRAFT);
-        ps.setDraft(true);
-      }
-
-      info = patchSetInfoFactory.get(c, ps.getId());
-      change.setCurrentPatchSet(info);
-      ChangeUtil.updated(change);
-      cmd = new ReceiveCommand(ObjectId.zeroId(), c, ps.getRefName());
+      ins = changeInserterFactory.create(ctl, change, c)
+          .setDraft(magicBranch.isDraft());
+      cmd = new ReceiveCommand(ObjectId.zeroId(), c,
+          ins.getPatchSet().getRefName());
     }
 
     CheckedFuture<Void, OrmException> insertChange() throws IOException {
@@ -1497,7 +1444,7 @@
       ListenableFuture<Void> future = changeUpdateExector.submit(
           requestScopePropagator.wrap(new Callable<Void>() {
         @Override
-        public Void call() throws OrmException {
+        public Void call() throws OrmException, IOException {
           if (caller == Thread.currentThread()) {
             insertChange(db);
           } else {
@@ -1517,7 +1464,8 @@
       return Futures.makeChecked(future, ORM_EXCEPTION);
     }
 
-    private void insertChange(ReviewDb db) throws OrmException {
+    private void insertChange(ReviewDb db) throws OrmException, IOException {
+      final PatchSet ps = ins.getPatchSet();
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = commit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
@@ -1527,9 +1475,16 @@
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
 
-      changeInserter.insertChange(db, change, ps, commit, labelTypes, info,
-          recipients.getReviewers());
+      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())
+        .setMessage(msg)
+        .setSendMail(false)
+        .insert();
       created = true;
 
       workQueue.getDefaultQueue()
@@ -1540,7 +1495,7 @@
             CreateChangeSender cm =
                 createChangeSenderFactory.create(change);
             cm.setFrom(me);
-            cm.setPatchSet(ps, info);
+            cm.setPatchSet(ps, ins.getPatchSetInfo());
             cm.addReviewers(recipients.getReviewers());
             cm.addExtraCC(recipients.getCcOnly());
             cm.send();
@@ -1561,7 +1516,8 @@
     }
   }
 
-  private void submit(ChangeControl changeCtl, PatchSet ps) throws OrmException {
+  private void submit(ChangeControl changeCtl, PatchSet ps)
+      throws OrmException, IOException {
     Submit submit = submitProvider.get();
     RevisionResource rsrc = new RevisionResource(new ChangeResource(changeCtl), ps);
     Change c = submit.submit(rsrc, currentUser);
@@ -1669,6 +1625,7 @@
     ChangeMessage msg;
     String mergedIntoRef;
     boolean skip;
+    ChangeKind changeKind;
     private PatchSet.Id priorPatchSet;
 
     ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
@@ -1677,6 +1634,7 @@
       this.newCommit = newCommit;
       this.inputCommand = cmd;
       this.checkMergedInto = checkMergedInto;
+      this.changeKind = ChangeKind.REWORK;
 
       revisions = HashBiMap.create();
       for (Ref ref : refs(toChange)) {
@@ -1739,22 +1697,26 @@
       if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) {
         return false;
       }
+      rp.getRevWalk().parseBody(priorCommit);
 
       // Don't allow the same tree if the commit message is unmodified
       // or no parents were updated (rebase), else warn that only part
       // of the commit was modified.
       if (newCommit.getTree() == priorCommit.getTree()) {
-        rp.getRevWalk().parseBody(priorCommit);
         final boolean messageEq =
             eq(newCommit.getFullMessage(), priorCommit.getFullMessage());
         final boolean parentsEq = parentsEqual(newCommit, priorCommit);
         final boolean authorEq = authorEqual(newCommit, priorCommit);
+        final ObjectReader reader = rp.getRevWalk().getObjectReader();
 
         if (messageEq && parentsEq && authorEq && !autoClose) {
+          addMessage(String.format(
+              "(W) No changes between prior commit %s and new commit %s",
+              reader.abbreviate(priorCommit).name(),
+              reader.abbreviate(newCommit).name()));
           reject(inputCommand, "no changes made");
           return false;
         } else {
-          ObjectReader reader = rp.getRevWalk().getObjectReader();
           StringBuilder msg = new StringBuilder();
           msg.append("(W) ");
           msg.append(reader.abbreviate(newCommit).name());
@@ -1773,10 +1735,14 @@
         }
       }
 
+      changeKind =
+          PatchSetInserter.getChangeKind(mergeUtilFactory,
+              projectControl.getProjectState(), repo, priorCommit, newCommit);
+
       PatchSet.Id id =
           ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId());
       newPatchSet = new PatchSet(id);
-      newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+      newPatchSet.setCreatedOn(TimeUtil.nowTs());
       newPatchSet.setUploader(currentUser.getAccountId());
       newPatchSet.setRevision(toRevId(newCommit));
       if (magicBranch != null && magicBranch.isDraft()) {
@@ -1798,7 +1764,7 @@
       ListenableFuture<PatchSet.Id> future = changeUpdateExector.submit(
           requestScopePropagator.wrap(new Callable<PatchSet.Id>() {
         @Override
-        public PatchSet.Id call() throws OrmException {
+        public PatchSet.Id call() throws OrmException, IOException {
           try {
             if (caller == Thread.currentThread()) {
               return insertPatchSet(db);
@@ -1820,7 +1786,7 @@
       return Futures.makeChecked(future, ORM_EXCEPTION);
     }
 
-    PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException {
+    PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException {
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = newCommit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
@@ -1851,7 +1817,7 @@
         final MailRecipients oldRecipients = getRecipientsFromApprovals(
             oldChangeApprovals);
         ApprovalsUtil.copyLabels(db, labelTypes, oldChangeApprovals,
-            priorPatchSet, newPatchSet.getId());
+            priorPatchSet, newPatchSet, changeKind);
         approvalsUtil.addReviewers(db, labelTypes, change, newPatchSet, info,
             recipients.getReviewers(), oldRecipients.getAll());
         recipients.add(oldRecipients);
@@ -1924,6 +1890,7 @@
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
+      CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
           ObjectId.zeroId(), newCommit);
       hooks.doPatchsetCreatedHook(change, newPatchSet, db);
@@ -1957,6 +1924,7 @@
           return "send-email newpatchset";
         }
       }));
+      indexFuture.checkedGet();
 
       if (magicBranch != null && magicBranch.isSubmit()) {
         submit(changeCtl, newPatchSet);
@@ -2019,7 +1987,7 @@
 
   private Ref findMergedInto(final String first, final RevCommit commit) {
     try {
-      final Map<String, Ref> all = repo.getAllRefs();
+      final Map<String, Ref> all = repo.getRefDatabase().getRefs(ALL);
       Ref firstRef = all.get(first);
       if (firstRef != null && isMergedInto(commit, firstRef)) {
         return firstRef;
@@ -2057,6 +2025,7 @@
       return;
     }
 
+    boolean defaultName = Strings.isNullOrEmpty(currentUser.getAccount().getFullName());
     final RevWalk walk = rp.getRevWalk();
     walk.reset();
     walk.sort(RevSort.NONE);
@@ -2072,6 +2041,23 @@
         } else if (!validCommit(ctl, cmd, c)) {
           break;
         }
+
+        if (defaultName && currentUser.getEmailAddresses().contains(
+              c.getCommitterIdent().getEmailAddress())) {
+          try {
+            Account a = db.accounts().get(currentUser.getAccountId());
+            if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
+              a.setFullName(c.getCommitterIdent().getName());
+              db.accounts().update(Collections.singleton(a));
+              currentUser.getAccount().setFullName(a.getFullName());
+              accountCache.evict(a.getId());
+            }
+          } catch (OrmException e) {
+            log.warn("Cannot default full_name", e);
+          } finally {
+            defaultName = false;
+          }
+        }
       }
     } catch (IOException err) {
       cmd.setResult(REJECTED_MISSING_OBJECT);
@@ -2173,7 +2159,7 @@
   }
 
   private Change.Key closeChange(final ReceiveCommand cmd, final PatchSet.Id psi,
-      final RevCommit commit) throws OrmException {
+      final RevCommit commit) throws OrmException, IOException {
     final String refName = cmd.getRefName();
     final Change.Id cid = psi.getParentKey();
 
@@ -2195,6 +2181,7 @@
 
     ReplaceRequest result = new ReplaceRequest(cid, commit, cmd, false);
     result.change = change;
+    result.changeCtl = projectControl.controlFor(change);
     result.newPatchSet = ps;
     result.info = patchSetInfoFactory.get(commit, psi);
     result.mergedIntoRef = refName;
@@ -2227,8 +2214,9 @@
   }
 
   private void markChangeMergedByPush(final ReviewDb db,
-      final ReplaceRequest result) throws OrmException {
-    final Change change = result.change;
+      final ReplaceRequest result) throws OrmException,
+      IOException {
+    Change change = result.change;
     final String mergedIntoRef = result.mergedIntoRef;
 
     change.setCurrentPatchSet(result.info);
@@ -2249,24 +2237,26 @@
       }
     }
     msgBuf.append(".");
-    final ChangeMessage msg =
-        new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-            .messageUUID(db)), currentUser.getAccountId(), result.info.getKey());
+    final ChangeMessage msg = new ChangeMessage(
+        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
+        currentUser.getAccountId(), TimeUtil.nowTs(), result.info.getKey());
     msg.setMessage(msgBuf.toString());
 
     db.changeMessages().insert(Collections.singleton(msg));
 
-    db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-      @Override
-      public Change update(Change change) {
-        if (change.getStatus().isOpen()) {
-          change.setCurrentPatchSet(result.info);
-          change.setStatus(Change.Status.MERGED);
-          ChangeUtil.updated(change);
-        }
-        return change;
-      }
-    });
+    change = db.changes().atomicUpdate(
+        change.getId(), new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus().isOpen()) {
+              change.setCurrentPatchSet(result.info);
+              change.setStatus(Change.Status.MERGED);
+              ChangeUtil.updated(change);
+            }
+            return change;
+          }
+        });
+    indexer.index(change);
   }
 
   private void sendMergedEmail(final ReplaceRequest result) {
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 0eb9b61..530a388 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
@@ -14,18 +14,47 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.collect.Maps;
-import com.google.gerrit.server.util.MagicBranch;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.gwtorm.server.OrmException;
+
+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.Map;
+import java.util.Set;
 
 /** Exposes only the non refs/changes/ reference names. */
 public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
+  private static final Logger log = LoggerFactory
+      .getLogger(ReceiveCommitsAdvertiseRefsHook.class);
+
+  private final ReviewDb db;
+  private final Project.NameKey projectName;
+
+  public ReceiveCommitsAdvertiseRefsHook(ReviewDb db,
+      Project.NameKey projectName) {
+    this.db = db;
+    this.projectName = projectName;
+  }
+
   @Override
   public void advertiseRefs(UploadPack us) {
     throw new UnsupportedOperationException(
@@ -33,10 +62,18 @@
   }
 
   @Override
-  public void advertiseRefs(BaseReceivePack rp) {
+  public void advertiseRefs(BaseReceivePack rp)
+      throws ServiceMayNotContinueException {
     Map<String, Ref> oldRefs = rp.getAdvertisedRefs();
     if (oldRefs == null) {
-      oldRefs = rp.getRepository().getAllRefs();
+      try {
+        oldRefs = rp.getRepository().getRefDatabase().getRefs(ALL);
+      } catch (IOException e) {
+        ServiceMayNotContinueException ex =
+            new ServiceMayNotContinueException(e.getMessage());
+        ex.initCause(e);
+        throw ex;
+      }
     }
     Map<String, Ref> r = Maps.newHashMapWithExpectedSize(oldRefs.size());
     for (Map.Entry<String, Ref> e : oldRefs.entrySet()) {
@@ -45,7 +82,84 @@
         r.put(name, e.getValue());
       }
     }
-    rp.setAdvertisedRefs(r, rp.getAdvertisedObjects());
+    rp.setAdvertisedRefs(r, advertiseHistory(r.values(), rp));
+  }
+
+  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.
+    try {
+      Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(32);
+      for (Change c : db.changes().byProjectOpenNext(projectName, "z", 32)) {
+        PatchSet.Id id = c.currentPatchSetId();
+        if (id != null) {
+          toGet.add(id);
+        }
+      }
+      for (PatchSet ps : db.patchSets().get(toGet)) {
+        if (ps.getRevision() != null && ps.getRevision().get() != null) {
+          toInclude.add(ObjectId.fromString(ps.getRevision().get()));
+        }
+      }
+    } catch (OrmException err) {
+      log.error("Cannot list open changes of " + projectName, err);
+    }
+
+    // Size of an additional ".have" line.
+    final int haveLineLen = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1;
+
+    // Maximum number of bytes to "waste" in the advertisement with
+    // a peek at this repository's current reachable history.
+    final int maxExtraSize = 8192;
+
+    // Number of recent commits to advertise immediately, hoping to
+    // show a client a nearby merge base.
+    final int base = 64;
+
+    // Number of commits to skip once base has already been shown.
+    final int step = 16;
+
+    // Total number of commits to extract from the history.
+    final int max = maxExtraSize / haveLineLen;
+
+    // Scan history until the advertisement is full.
+    Set<ObjectId> alreadySending = Sets.newHashSet();
+    RevWalk rw = rp.getRevWalk();
+    for (Ref ref : sending) {
+      try {
+        if (ref.getObjectId() != null) {
+          alreadySending.add(ref.getObjectId());
+          rw.markStart(rw.parseCommit(ref.getObjectId()));
+        }
+      } catch (IOException badCommit) {
+        continue;
+      }
+    }
+
+    int stepCnt = 0;
+    RevCommit c;
+    try {
+      while ((c = rw.next()) != null && toInclude.size() < max) {
+        if (alreadySending.contains(c)) {
+        } else if (toInclude.contains(c)) {
+        } else if (c.getParentCount() > 1) {
+        } else if (toInclude.size() < base) {
+          toInclude.add(c);
+        } else {
+          stepCnt = ++stepCnt % step;
+          if (stepCnt == 0) {
+            toInclude.add(c);
+          }
+        }
+      }
+    } catch (IOException err) {
+      log.error("Error trying to advertise history on " + projectName, err);
+    }
+    rw.reset();
+    return toInclude;
   }
 
   private static boolean skip(String name) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
index 7c2ba86..9626e23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.ChangeIndexer;
 
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -55,13 +56,15 @@
     protected final Set<RevCommit> alreadyAccepted;
     protected final Branch.NameKey destBranch;
     protected final MergeUtil mergeUtil;
+    protected final ChangeIndexer indexer;
     protected final MergeSorter mergeSorter;
 
     Arguments(final IdentifiedUser.GenericFactory identifiedUserFactory,
         final PersonIdent myIdent, final ReviewDb db, final Repository repo,
         final RevWalk rw, final ObjectInserter inserter,
         final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
-        final Branch.NameKey destBranch, final MergeUtil mergeUtil) {
+        final Branch.NameKey destBranch, final MergeUtil mergeUtil,
+        final ChangeIndexer indexer) {
       this.identifiedUserFactory = identifiedUserFactory;
       this.myIdent = myIdent;
       this.db = db;
@@ -73,6 +76,7 @@
       this.alreadyAccepted = alreadyAccepted;
       this.destBranch = destBranch;
       this.mergeUtil = mergeUtil;
+      this.indexer = indexer;
       this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
index 8bf831c..701a925 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -22,6 +23,7 @@
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -40,8 +42,6 @@
 
 import java.util.Set;
 
-import javax.annotation.Nullable;
-
 /** Factory to create a {@link SubmitStrategy} for a {@link SubmitType}. */
 public class SubmitStrategyFactory {
   private static final Logger log = LoggerFactory
@@ -54,6 +54,7 @@
   private final RebaseChange rebaseChange;
   private final ProjectCache projectCache;
   private final MergeUtil.Factory mergeUtilFactory;
+  private final ChangeIndexer indexer;
 
   @Inject
   SubmitStrategyFactory(
@@ -63,7 +64,8 @@
       @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
       final GitReferenceUpdated gitRefUpdated, final RebaseChange rebaseChange,
       final ProjectCache projectCache,
-      final MergeUtil.Factory mergeUtilFactory) {
+      final MergeUtil.Factory mergeUtilFactory,
+      final ChangeIndexer indexer) {
     this.identifiedUserFactory = identifiedUserFactory;
     this.myIdent = myIdent;
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -71,6 +73,7 @@
     this.rebaseChange = rebaseChange;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
+    this.indexer = indexer;
   }
 
   public SubmitStrategy create(final SubmitType submitType, final ReviewDb db,
@@ -82,7 +85,7 @@
     final SubmitStrategy.Arguments args =
         new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db, repo,
             rw, inserter, canMergeFlag, alreadyAccepted, destBranch,
-            mergeUtilFactory.create(project));
+            mergeUtilFactory.create(project), indexer);
     switch (submitType) {
       case CHERRY_PICK:
         return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
@@ -93,7 +96,7 @@
       case MERGE_IF_NECESSARY:
         return new MergeIfNecessary(args);
       case REBASE_IF_NECESSARY:
-        return new RebaseIfNecessary(args, rebaseChange);
+        return new RebaseIfNecessary(args, rebaseChange, myIdent);
       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/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index eeab8f3..2025225 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+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;
@@ -62,8 +63,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import javax.annotation.Nullable;
-
 public class SubmoduleOp {
   public interface Factory {
     SubmoduleOp create(Branch.NameKey destBranch, RevCommit mergeTip,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
index 3c64229..dec1768 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.git;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
index 7d95db2..b63378f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.git;
 
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 c57942c..3eb9273 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
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.git;
 
@@ -26,6 +26,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -159,7 +160,7 @@
     TagWalk rw = new TagWalk(git);
     rw.setRetainBody(false);
     try {
-      for (Ref ref : git.getAllRefs().values()) {
+      for (Ref ref : git.getRefDatabase().getRefs(RefDatabase.ALL).values()) {
         if (skip(ref)) {
           continue;
 
@@ -186,7 +187,7 @@
         }
       }
     } catch (IOException e) {
-      log.warn("Repository " + projectName + " has corruption", e);
+      log.warn("Error building tags for repository " + projectName, e);
     } finally {
       rw.release();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
index d5120e0..d923e51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.git;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
index af404b5..8be0a10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -29,12 +30,14 @@
   private final int timeout;
   private final PackConfig packConfig;
   private final long maxObjectSizeLimit;
+  private final String maxObjectSizeLimitFormatted;
 
   @Inject
   TransferConfig(@GerritServerConfig final Config cfg) {
     timeout = (int) ConfigUtil.getTimeUnit(cfg, "transfer", null, "timeout", //
         0, TimeUnit.SECONDS);
     maxObjectSizeLimit = cfg.getLong("receive", "maxObjectSizeLimit", 0);
+    maxObjectSizeLimitFormatted = cfg.getString("receive", null, "maxObjectSizeLimit");
 
     packConfig = new PackConfig();
     packConfig.setDeltaCompress(false);
@@ -54,4 +57,19 @@
   public long getMaxObjectSizeLimit() {
     return maxObjectSizeLimit;
   }
+
+  public String getFormattedMaxObjectSizeLimit() {
+    return maxObjectSizeLimitFormatted;
+  }
+
+  public long getEffectiveMaxObjectSizeLimit(ProjectState p) {
+    long global = getMaxObjectSizeLimit();
+    long local = p.getMaxObjectSizeLimit();
+    if (global > 0 && local > 0) {
+      return Math.min(global, local);
+    } else {
+      // zero means "no limit", in this case the max is more limiting
+      return Math.max(global, local);
+    }
+  }
 }
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 5cd0def..8c65b1a 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
@@ -24,12 +24,15 @@
 import com.google.gwtorm.server.OrmException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.AbstractAdvertiseRefsHook;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -120,9 +123,16 @@
   }
 
   @Override
-  protected Map<String, Ref> getAdvertisedRefs(
-      Repository repository, RevWalk revWalk) {
-    return filter(repository.getAllRefs());
+  protected Map<String, Ref> getAdvertisedRefs(Repository repository,
+      RevWalk revWalk) throws ServiceMayNotContinueException {
+    try {
+      return filter(repository.getRefDatabase().getRefs(RefDatabase.ALL));
+    } catch (IOException e) {
+      ServiceMayNotContinueException ex =
+          new ServiceMayNotContinueException(e.getMessage());
+      ex.initCause(e);
+      throw ex;
+    }
   }
 
   private Map<String, Ref> filter(Map<String, Ref> refs) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index bb11e62..64210a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -26,6 +26,7 @@
 
 import java.lang.Thread.UncaughtExceptionHandler;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
@@ -248,6 +249,7 @@
     private final Executor executor;
     private final int taskId;
     private final AtomicBoolean running;
+    private final Date startTime;
 
     Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor,
         int taskId) {
@@ -256,6 +258,7 @@
       this.executor = executor;
       this.taskId = taskId;
       this.running = new AtomicBoolean();
+      this.startTime = new Date();
     }
 
     public int getTaskId() {
@@ -281,6 +284,10 @@
       return State.OTHER;
     }
 
+    public Date getStartTime() {
+      return startTime;
+    }
+
     public boolean cancel(boolean mayInterruptIfRunning) {
       if (task.cancel(mayInterruptIfRunning)) {
         // Tiny abuse of running: if the task needs to know it was
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
index 8f11243..e98c49b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
@@ -30,7 +30,7 @@
   /**
    * Commit validation.
    *
-   * @param received commit event details
+   * @param receiveEvent commit event details
    * @return list of validation messages
    * @throws CommitValidationException if validation fails
    */
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 d782cf0..10fa148 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,11 +14,14 @@
 
 package com.google.gerrit.server.git.validators;
 
+import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.GerritPersonIdent;
 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.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -34,6 +37,7 @@
 import com.jcraft.jsch.HostKey;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterKey;
@@ -49,14 +53,15 @@
 import java.util.LinkedList;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 public class CommitValidators {
   private static final Logger log = LoggerFactory
       .getLogger(CommitValidators.class);
 
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
+  private static final String GIT_HOOKS_COMMIT_MSG =
+      "`git rev-parse --git-dir`/hooks/commit-msg";
+
   public interface Factory {
     CommitValidators create(RefControl refControl, SshInfo sshInfo,
         Repository repo);
@@ -65,6 +70,7 @@
   private final PersonIdent gerritIdent;
   private final RefControl refControl;
   private final String canonicalWebUrl;
+  private final String installCommitMsgHookCommand;
   private final SshInfo sshInfo;
   private final Repository repo;
   private final DynamicSet<CommitValidationListener> commitValidationListeners;
@@ -72,12 +78,15 @@
   @Inject
   CommitValidators(@GerritPersonIdent final PersonIdent gerritIdent,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
+      @GerritServerConfig final Config config,
       final DynamicSet<CommitValidationListener> commitValidationListeners,
       @Assisted final SshInfo sshInfo,
       @Assisted final Repository repo, @Assisted final RefControl refControl) {
     this.gerritIdent = gerritIdent;
     this.refControl = refControl;
     this.canonicalWebUrl = canonicalWebUrl;
+    this.installCommitMsgHookCommand =
+        config.getString("gerrit", null, "installCommitMsgHookCommand");
     this.sshInfo = sshInfo;
     this.repo = repo;
     this.commitValidationListeners = commitValidationListeners;
@@ -98,7 +107,8 @@
     if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
         || ReceiveCommits.NEW_PATCHSET.matcher(
             receiveEvent.command.getRefName()).matches()) {
-      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
+          installCommitMsgHookCommand, sshInfo));
     }
     validators.add(new ConfigValidator(refControl, repo));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
@@ -132,7 +142,8 @@
     if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
         || ReceiveCommits.NEW_PATCHSET.matcher(
             receiveEvent.command.getRefName()).matches()) {
-      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
+          installCommitMsgHookCommand, sshInfo));
     }
     validators.add(new ConfigValidator(refControl, repo));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
@@ -153,23 +164,24 @@
   }
 
   public static class ChangeIdValidator implements CommitValidationListener {
-    private final RefControl refControl;
+    private final ProjectControl projectControl;
     private final String canonicalWebUrl;
+    private final String installCommitMsgHookCommand;
     private final SshInfo sshInfo;
+    private final IdentifiedUser user;
 
     public ChangeIdValidator(RefControl refControl, String canonicalWebUrl,
-        SshInfo sshInfo) {
-      this.refControl = refControl;
+        String installCommitMsgHookCommand, SshInfo sshInfo) {
+      this.projectControl = refControl.getProjectControl();
       this.canonicalWebUrl = canonicalWebUrl;
+      this.installCommitMsgHookCommand = installCommitMsgHookCommand;
       this.sshInfo = sshInfo;
+      this.user = (IdentifiedUser) projectControl.getCurrentUser();
     }
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-
-      final ProjectControl projectControl = refControl.getProjectControl();
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
       final List<String> idList = receiveEvent.commit.getFooterLines(CHANGE_ID);
 
       List<CommitValidationMessage> messages =
@@ -178,8 +190,8 @@
       if (idList.isEmpty()) {
         if (projectControl.getProjectState().isRequireChangeID()) {
           String errMsg = "missing Change-Id in commit message footer";
-          messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
-              currentUser, canonicalWebUrl, sshInfo));
+          messages.add(getFixedCommitMsgWithChangeId(
+              errMsg, receiveEvent.commit));
           throw new CommitValidationException(errMsg, messages);
         }
       } else if (idList.size() > 1) {
@@ -190,13 +202,99 @@
         if (!v.matches("^I[0-9a-f]{8,}.*$")) {
           final String errMsg =
               "missing or invalid Change-Id line format in commit message footer";
-          messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
-              currentUser, canonicalWebUrl, sshInfo));
+          messages.add(
+              getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit));
           throw new CommitValidationException(errMsg, messages);
         }
       }
       return Collections.<CommitValidationMessage>emptyList();
     }
+
+    /**
+     * We handle 3 cases:
+     * 1. No change id in the commit message at all.
+     * 2. Change id last in the commit message but missing empty line to create the footer.
+     * 3. There is a change-id somewhere in the commit message, but we ignore it.
+     *
+     * @return The fixed up commit message
+     */
+    private CommitValidationMessage getFixedCommitMsgWithChangeId(
+        final String errMsg, final RevCommit c) {
+      final String changeId = "Change-Id:";
+      StringBuilder sb = new StringBuilder();
+      sb.append("ERROR: ").append(errMsg);
+      sb.append('\n');
+      sb.append("Suggestion for commit message:\n");
+
+      if (c.getFullMessage().indexOf(changeId) == -1) {
+        sb.append(c.getFullMessage());
+        sb.append('\n');
+        sb.append(changeId).append(" I").append(c.name());
+      } else {
+        String lines[] = c.getFullMessage().trim().split("\n");
+        String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
+
+        if (lastLine.indexOf(changeId) == 0) {
+          for (int i = 0; i < lines.length - 1; i++) {
+            sb.append(lines[i]);
+            sb.append('\n');
+          }
+
+          sb.append('\n');
+          sb.append(lastLine);
+        } else {
+          sb.append(c.getFullMessage());
+          sb.append('\n');
+          sb.append(changeId).append(" I").append(c.name());
+          sb.append('\n');
+          sb.append("Hint: A potential Change-Id was found, but it was not in the ");
+          sb.append("footer (last paragraph) of the commit message.");
+        }
+      }
+      sb.append('\n');
+      sb.append('\n');
+      sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
+      sb.append(getCommitMessageHookInstallationHint()).append('\n');
+      sb.append('\n');
+
+      return new CommitValidationMessage(sb.toString(), false);
+    }
+
+    private String getCommitMessageHookInstallationHint() {
+      if (installCommitMsgHookCommand != null) {
+        return installCommitMsgHookCommand;
+      }
+      final List<HostKey> hostKeys = sshInfo.getHostKeys();
+
+      // If there are no SSH keys, the commit-msg hook must be installed via
+      // HTTP(S)
+      String p = GIT_HOOKS_COMMIT_MSG;
+      if (hostKeys.isEmpty()) {
+        return String.format(
+            "  curl -Lo %s %s/tools/hooks/commit-msg ; chmod +x %s", p,
+            getGerritUrl(canonicalWebUrl), p);
+      }
+
+      // SSH keys exist, so the hook can be installed with scp.
+      String sshHost;
+      int sshPort;
+      String host = hostKeys.get(0).getHost();
+      int c = host.lastIndexOf(':');
+      if (0 <= c) {
+        if (host.startsWith("*:")) {
+          sshHost = getGerritHost(canonicalWebUrl);
+        } else {
+          sshHost = host.substring(0, c);
+        }
+        sshPort = Integer.parseInt(host.substring(c + 1));
+      } else {
+        sshHost = host;
+        sshPort = 22;
+      }
+
+      return String.format("  scp -p -P %d %s@%s:hooks/commit-msg %s",
+          sshPort, user.getUserName(), sshHost, p);
+    }
   }
 
   /**
@@ -448,105 +546,15 @@
   }
 
   /**
-   * We handle 3 cases:
-   * 1. No change id in the commit message at all.
-   * 2. Change id last in the commit message but missing empty line to create the footer.
-   * 3. There is a change-id somewhere in the commit message, but we ignore it.
-   *
-   * @return The fixed up commit message
-   */
-  private static CommitValidationMessage getFixedCommitMsgWithChangeId(final String errMsg,
-      final RevCommit c, final IdentifiedUser currentUser,
-      String canonicalWebUrl, final SshInfo sshInfo) {
-    final String changeId = "Change-Id:";
-    StringBuilder sb = new StringBuilder();
-    sb.append("ERROR: ").append(errMsg);
-    sb.append('\n');
-    sb.append("Suggestion for commit message:\n");
-
-    if (c.getFullMessage().indexOf(changeId) == -1) {
-      sb.append(c.getFullMessage());
-      sb.append('\n');
-      sb.append(changeId).append(" I").append(c.name());
-    } else {
-      String lines[] = c.getFullMessage().trim().split("\n");
-      String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
-
-      if (lastLine.indexOf(changeId) == 0) {
-        for (int i = 0; i < lines.length - 1; i++) {
-          sb.append(lines[i]);
-          sb.append('\n');
-        }
-
-        sb.append('\n');
-        sb.append(lastLine);
-      } else {
-        sb.append(c.getFullMessage());
-        sb.append('\n');
-        sb.append(changeId).append(" I").append(c.name());
-        sb.append('\n');
-        sb.append("Hint: A potential Change-Id was found, but it was not in the ");
-        sb.append("footer (last paragraph) of the commit message.");
-      }
-    }
-    sb.append('\n');
-    sb.append('\n');
-    sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
-    sb.append(getCommitMessageHookInstallationHint(currentUser,
-        canonicalWebUrl, sshInfo)).append('\n');
-    sb.append('\n');
-
-    return new CommitValidationMessage(sb.toString(), false);
-  }
-
-  private static String getCommitMessageHookInstallationHint(
-      final IdentifiedUser currentUser, String canonicalWebUrl,
-      final SshInfo sshInfo) {
-    final List<HostKey> hostKeys = sshInfo.getHostKeys();
-
-    // If there are no SSH keys, the commit-msg hook must be installed via
-    // HTTP(S)
-    if (hostKeys.isEmpty()) {
-      String p = "$gitdir/hooks/commit-msg";
-      return String.format(
-          "  gitdir=$(git rev-parse --git-dir) curl -o %s %s/tools/hooks/commit-msg ; chmod +x %s", p,
-          getGerritUrl(canonicalWebUrl), p);
-    }
-
-    // SSH keys exist, so the hook can be installed with scp.
-    String sshHost;
-    int sshPort;
-    String host = hostKeys.get(0).getHost();
-    int c = host.lastIndexOf(':');
-    if (0 <= c) {
-      if (host.startsWith("*:")) {
-        sshHost = getGerritHost(canonicalWebUrl);
-      } else {
-        sshHost = host.substring(0, c);
-      }
-      sshPort = Integer.parseInt(host.substring(c + 1));
-    } else {
-      sshHost = host;
-      sshPort = 22;
-    }
-
-    return String.format("  gitdir=$(git rev-parse --git-dir) scp -p -P %d %s@%s:hooks/commit-msg $gitdir/hooks/",
-        sshPort, currentUser.getUserName(), sshHost);
-  }
-
-  /**
    * Get the Gerrit URL.
    *
    * @return the canonical URL (with any trailing slash removed) if it is
    *         configured, otherwise fall back to "http://hostname" where hostname
-   *         is the value returned by {@link #getGerritHost()}.
+   *         is the value returned by {@link #getGerritHost(String)}.
    */
   private static String getGerritUrl(String canonicalWebUrl) {
     if (canonicalWebUrl != null) {
-      if (canonicalWebUrl.endsWith("/")) {
-        return canonicalWebUrl.substring(0, canonicalWebUrl.lastIndexOf("/"));
-      }
-      return canonicalWebUrl;
+      return CharMatcher.is('/').trimTrailingFrom(canonicalWebUrl);
     } else {
       return "http://" + getGerritHost(canonicalWebUrl);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java
new file mode 100644
index 0000000..78819a8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java
@@ -0,0 +1,31 @@
+// 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.git.validators;
+
+import com.google.gerrit.server.git.CommitMergeStatus;
+
+public class MergeValidationException extends Exception {
+  private static final long serialVersionUID = 1L;
+  private final CommitMergeStatus status;
+
+  public MergeValidationException(CommitMergeStatus status) {
+    super(status.toString());
+    this.status = status;
+  }
+
+  public CommitMergeStatus getStatus() {
+    return status;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
new file mode 100644
index 0000000..0a8d245
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
@@ -0,0 +1,48 @@
+// 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.git.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.project.ProjectState;
+
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Listener to provide validation of commits before merging.
+ *
+ * Invoked by Gerrit before a commit is merged.
+ */
+@ExtensionPoint
+public interface MergeValidationListener {
+  /**
+   * Validate a commit before it is merged.
+   *
+   * @param repo the repository
+   * @param commit commit details
+   * @param destProject the destination project
+   * @param destBranch the destination branch
+   * @param patchSetId the patch set ID
+   * @throws MergeValidationException if the commit fails to validate
+   */
+  public void onPreMerge(Repository repo,
+      CodeReviewCommit commit,
+      ProjectState destProject,
+      Branch.NameKey destBranch,
+      PatchSet.Id patchSetId)
+      throws MergeValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
new file mode 100644
index 0000000..6eed909
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -0,0 +1,164 @@
+// 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.git.validators;
+
+import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CommitMergeStatus;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Repository;
+
+import java.util.List;
+
+public class MergeValidators {
+  private final DynamicSet<MergeValidationListener> mergeValidationListeners;
+  private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
+
+  public interface Factory {
+    MergeValidators create();
+  }
+
+  @Inject
+  MergeValidators(DynamicSet<MergeValidationListener> mergeValidationListeners,
+      ProjectConfigValidator.Factory projectConfigValidatorFactory) {
+    this.mergeValidationListeners = mergeValidationListeners;
+    this.projectConfigValidatorFactory = projectConfigValidatorFactory;
+  }
+
+  public void validatePreMerge(Repository repo,
+      CodeReviewCommit commit,
+      ProjectState destProject,
+      Branch.NameKey destBranch,
+      PatchSet.Id patchSetId)
+      throws MergeValidationException {
+    List<MergeValidationListener> validators = Lists.newLinkedList();
+
+    validators.add(new PluginMergeValidationListener(mergeValidationListeners));
+    validators.add(projectConfigValidatorFactory.create());
+
+    for (MergeValidationListener validator : validators) {
+      validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId);
+    }
+  }
+
+  public static class ProjectConfigValidator implements
+      MergeValidationListener {
+    private final AllProjectsName allProjectsName;
+    private final ReviewDb db;
+    private final ProjectCache projectCache;
+    private final IdentifiedUser.GenericFactory identifiedUserFactory;
+
+    public interface Factory {
+      ProjectConfigValidator create();
+    }
+
+    @Inject
+    public ProjectConfigValidator(AllProjectsName allProjectsName,
+        ReviewDb db, ProjectCache projectCache,
+        IdentifiedUser.GenericFactory iuf) {
+      this.allProjectsName = allProjectsName;
+      this.db = db;
+      this.projectCache = projectCache;
+      this.identifiedUserFactory = iuf;
+    }
+
+    @Override
+    public void onPreMerge(final Repository repo,
+        final CodeReviewCommit commit,
+        final ProjectState destProject,
+        final Branch.NameKey destBranch,
+        final PatchSet.Id patchSetId)
+        throws MergeValidationException {
+      if (GitRepositoryManager.REF_CONFIG.equals(destBranch.get())) {
+        final Project.NameKey newParent;
+        try {
+          ProjectConfig cfg =
+              new ProjectConfig(destProject.getProject().getNameKey());
+          cfg.load(repo, commit);
+          newParent = cfg.getProject().getParent(allProjectsName);
+        } catch (Exception e) {
+          throw new MergeValidationException(CommitMergeStatus.
+              INVALID_PROJECT_CONFIGURATION);
+        }
+        final Project.NameKey oldParent =
+            destProject.getProject().getParent(allProjectsName);
+        if (oldParent == null) {
+          // update of the 'All-Projects' project
+          if (newParent != null) {
+            throw new MergeValidationException(CommitMergeStatus.
+                INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT);
+          }
+        } else {
+          if (!oldParent.equals(newParent)) {
+            final PatchSetApproval psa = getSubmitter(db, patchSetId);
+            if (psa == null) {
+              throw new MergeValidationException(CommitMergeStatus.
+                  SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
+            }
+            final IdentifiedUser submitter =
+                identifiedUserFactory.create(psa.getAccountId());
+            if (!submitter.getCapabilities().canAdministrateServer()) {
+              throw new MergeValidationException(CommitMergeStatus.
+                  SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
+            }
+
+            if (projectCache.get(newParent) == null) {
+              throw new MergeValidationException(CommitMergeStatus.
+                  INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /** Execute merge validation plug-ins */
+  public static class PluginMergeValidationListener implements
+      MergeValidationListener {
+    private final DynamicSet<MergeValidationListener> mergeValidationListeners;
+
+    public PluginMergeValidationListener(
+        DynamicSet<MergeValidationListener> mergeValidationListeners) {
+      this.mergeValidationListeners = mergeValidationListeners;
+    }
+
+    @Override
+    public void onPreMerge(Repository repo,
+        CodeReviewCommit commit,
+        ProjectState destProject,
+        Branch.NameKey destBranch,
+        PatchSet.Id patchSetId)
+        throws MergeValidationException {
+      for (MergeValidationListener validator : mergeValidationListeners) {
+        validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId);
+      }
+    }
+  }
+}
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 aa3f30e..195ea1c 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
@@ -27,14 +27,15 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -43,12 +44,18 @@
 import java.util.Map;
 
 public class AddIncludedGroups implements RestModifyView<GroupResource, Input> {
-  static class Input {
+  public static class Input {
     @DefaultInput
     String _oneGroup;
 
     List<String> groups;
 
+    public static Input fromGroups(List<String> groups) {
+      Input in = new Input();
+      in.groups = groups;
+      return in;
+    }
+
     static Input init(Input in) {
       if (in == null) {
         in = new Input();
@@ -89,8 +96,8 @@
     input = Input.init(input);
 
     GroupControl control = resource.getControl();
-    Map<AccountGroup.UUID, AccountGroupIncludeByUuid> newIncludedGroups = Maps.newHashMap();
-    List<AccountGroupIncludeByUuidAudit> newIncludedGroupsAudits = Lists.newLinkedList();
+    Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = Maps.newHashMap();
+    List<AccountGroupByIdAud> newIncludedGroupsAudits = Lists.newLinkedList();
     List<GroupInfo> result = Lists.newLinkedList();
     Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
 
@@ -102,23 +109,24 @@
       }
 
       if (!newIncludedGroups.containsKey(d.getGroupUUID())) {
-        AccountGroupIncludeByUuid.Key agiKey =
-            new AccountGroupIncludeByUuid.Key(group.getId(),
+        AccountGroupById.Key agiKey =
+            new AccountGroupById.Key(group.getId(),
                 d.getGroupUUID());
-        AccountGroupIncludeByUuid agi = db.accountGroupIncludesByUuid().get(agiKey);
+        AccountGroupById agi = db.accountGroupById().get(agiKey);
         if (agi == null) {
-          agi = new AccountGroupIncludeByUuid(agiKey);
+          agi = new AccountGroupById(agiKey);
           newIncludedGroups.put(d.getGroupUUID(), agi);
-          newIncludedGroupsAudits.add(new AccountGroupIncludeByUuidAudit(agi, me));
+          newIncludedGroupsAudits.add(
+              new AccountGroupByIdAud(agi, me, TimeUtil.nowTs()));
         }
       }
       result.add(json.format(d));
     }
 
     if (!newIncludedGroups.isEmpty()) {
-      db.accountGroupIncludesByUuidAudit().insert(newIncludedGroupsAudits);
-      db.accountGroupIncludesByUuid().insert(newIncludedGroups.values());
-      for (AccountGroupIncludeByUuid agi : newIncludedGroups.values()) {
+      db.accountGroupByIdAud().insert(newIncludedGroupsAudits);
+      db.accountGroupById().insert(newIncludedGroups.values());
+      for (AccountGroupById agi : newIncludedGroups.values()) {
         groupIncludeCache.evictMemberIn(agi.getIncludeUUID());
       }
       groupIncludeCache.evictMembersOf(group.getGroupUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 7f32a4d..5ebf504 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.group.AddMembers.Input;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -46,12 +47,17 @@
 import java.util.List;
 import java.util.Map;
 
-class AddMembers implements RestModifyView<GroupResource, Input> {
-  static class Input {
+public class AddMembers implements RestModifyView<GroupResource, Input> {
+  public static class Input {
     @DefaultInput
     String _oneMember;
 
     List<String> members;
+    public static Input fromMembers(List<String> members) {
+      Input in = new Input();
+      in.members = members;
+      return in;
+    }
 
     static Input init(Input in) {
       if (in == null) {
@@ -72,6 +78,7 @@
   private final Provider<AccountsCollection> accounts;
   private final AccountResolver accountResolver;
   private final AccountCache accountCache;
+  private final AccountInfo.Loader.Factory infoFactory;
   private final ReviewDb db;
 
   @Inject
@@ -80,12 +87,14 @@
       Provider<AccountsCollection> accounts,
       AccountResolver accountResolver,
       AccountCache accountCache,
+      AccountInfo.Loader.Factory infoFactory,
       ReviewDb db) {
     this.accountManager = accountManager;
     this.authType = authConfig.getAuthType();
     this.accounts = accounts;
     this.accountResolver = accountResolver;
     this.accountCache = accountCache;
+    this.infoFactory = infoFactory;
     this.db = db;
   }
 
@@ -104,6 +113,7 @@
     List<AccountGroupMemberAudit> newAccountGroupMemberAudits = Lists.newLinkedList();
     List<AccountInfo> result = Lists.newLinkedList();
     Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
+    AccountInfo.Loader loader = infoFactory.create(true);
 
     for (String nameOrEmail : input.members) {
       Account a = findAccount(nameOrEmail);
@@ -123,10 +133,11 @@
         if (m == null) {
           m = new AccountGroupMember(key);
           newAccountGroupMembers.put(m.getAccountId(), m);
-          newAccountGroupMemberAudits.add(new AccountGroupMemberAudit(m, me));
+          newAccountGroupMemberAudits.add(
+              new AccountGroupMemberAudit(m, me, TimeUtil.nowTs()));
         }
       }
-      result.add(AccountInfo.parse(a, true));
+      result.add(loader.get(a.getId()));
     }
 
     db.accountGroupMembersAudit().insert(newAccountGroupMemberAudits);
@@ -135,6 +146,7 @@
       accountCache.evict(m.getAccountId());
     }
 
+    loader.fill();
     return result;
   }
 
@@ -213,7 +225,8 @@
     }
 
     @Override
-    public Object apply(MemberResource resource, PutMember.Input input) {
+    public Object apply(MemberResource resource, PutMember.Input input)
+        throws OrmException {
       // Do nothing, the user is already a member.
       return get.get().apply(resource);
     }
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 15f9325..c5d95b0 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
@@ -26,14 +26,15 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 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;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -68,8 +69,8 @@
     input = Input.init(input);
 
     final GroupControl control = resource.getControl();
-    final Map<AccountGroup.UUID, AccountGroupIncludeByUuid> includedGroups = getIncludedGroups(internalGroup.getId());
-    final List<AccountGroupIncludeByUuid> toRemove = Lists.newLinkedList();
+    final Map<AccountGroup.UUID, AccountGroupById> includedGroups = getIncludedGroups(internalGroup.getId());
+    final List<AccountGroupById> toRemove = Lists.newLinkedList();
 
     for (final String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.get().parse(includedGroup);
@@ -78,7 +79,7 @@
             d.getName()));
       }
 
-      AccountGroupIncludeByUuid g = includedGroups.remove(d.getGroupUUID());
+      AccountGroupById g = includedGroups.remove(d.getGroupUUID());
       if (g != null) {
         toRemove.add(g);
       }
@@ -86,8 +87,8 @@
 
     if (!toRemove.isEmpty()) {
       writeAudits(toRemove);
-      db.accountGroupIncludesByUuid().delete(toRemove);
-      for (final AccountGroupIncludeByUuid g : toRemove) {
+      db.accountGroupById().delete(toRemove);
+      for (final AccountGroupById g : toRemove) {
         groupIncludeCache.evictMemberIn(g.getIncludeUUID());
       }
       groupIncludeCache.evictMembersOf(internalGroup.getGroupUUID());
@@ -96,24 +97,24 @@
     return Response.none();
   }
 
-  private Map<AccountGroup.UUID, AccountGroupIncludeByUuid> getIncludedGroups(
+  private Map<AccountGroup.UUID, AccountGroupById> getIncludedGroups(
       final AccountGroup.Id groupId) throws OrmException {
-    final Map<AccountGroup.UUID, AccountGroupIncludeByUuid> groups =
+    final Map<AccountGroup.UUID, AccountGroupById> groups =
         Maps.newHashMap();
-    for (final AccountGroupIncludeByUuid g : db.accountGroupIncludesByUuid().byGroup(groupId)) {
+    for (final AccountGroupById g : db.accountGroupById().byGroup(groupId)) {
       groups.put(g.getIncludeUUID(), g);
     }
     return groups;
   }
 
-  private void writeAudits(final List<AccountGroupIncludeByUuid> toBeRemoved)
+  private void writeAudits(final List<AccountGroupById> toBeRemoved)
       throws OrmException {
     final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
-    final List<AccountGroupIncludeByUuidAudit> auditUpdates = Lists.newLinkedList();
-    for (final AccountGroupIncludeByUuid g : toBeRemoved) {
-      AccountGroupIncludeByUuidAudit audit = null;
-      for (AccountGroupIncludeByUuidAudit a : db
-          .accountGroupIncludesByUuidAudit().byGroupInclude(g.getGroupId(),
+    final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
+    for (final AccountGroupById g : toBeRemoved) {
+      AccountGroupByIdAud audit = null;
+      for (AccountGroupByIdAud a : db
+          .accountGroupByIdAud().byGroupInclude(g.getGroupId(),
               g.getIncludeUUID())) {
         if (a.isActive()) {
           audit = a;
@@ -122,11 +123,11 @@
       }
 
       if (audit != null) {
-        audit.removed(me);
+        audit.removed(me, TimeUtil.nowTs());
         auditUpdates.add(audit);
       }
     }
-    db.accountGroupIncludesByUuidAudit().update(auditUpdates);
+    db.accountGroupByIdAud().update(auditUpdates);
   }
 
   static class DeleteIncludedGroup implements
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 07276ca..186d4b6 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.AddMembers.Input;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -107,10 +108,10 @@
       }
 
       if (audit != null) {
-        audit.removed(me);
+        audit.removed(me, TimeUtil.nowTs());
         auditUpdates.add(audit);
       } else {
-        audit = new AccountGroupMemberAudit(m, me);
+        audit = new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
         audit.removedLegacy();
         auditInserts.add(audit);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
index 5651001..d98dd24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
@@ -16,10 +16,22 @@
 
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
 public class GetMember implements RestReadView<MemberResource> {
+  private final AccountInfo.Loader.Factory infoFactory;
+
+  @Inject
+  GetMember(AccountInfo.Loader.Factory infoFactory) {
+    this.infoFactory = infoFactory;
+  }
+
   @Override
-  public AccountInfo apply(MemberResource resource) {
-    return AccountInfo.parse(resource.getMember().getAccount(), true);
+  public AccountInfo apply(MemberResource rsrc) throws OrmException {
+    AccountInfo.Loader loader = infoFactory.create(true);
+    AccountInfo info = loader.get(rsrc.getMember().getAccountId());
+    loader.fill();
+    return info;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
index a74e6ef..8c9056d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -126,10 +126,8 @@
     }
   }
 
-  public static class GroupInfo {
+  public static class GroupInfo extends GroupBaseInfo {
     final String kind = "gerritcodereview#group";
-    public String id;
-    public String name;
     public String url;
     public GroupOptionsInfo options;
 
@@ -143,4 +141,9 @@
     public List<AccountInfo> members;
     public List<GroupInfo> includes;
   }
+
+  public static class GroupBaseInfo {
+    public String id;
+    public String name;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
index 32513a7..54fc787 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
@@ -28,7 +28,7 @@
 
   private final GroupControl control;
 
-  GroupResource(GroupControl control) {
+  public GroupResource(GroupControl control) {
     this.control = control;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
index 918a530..91da748 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupControl;
@@ -68,7 +67,7 @@
     final CurrentUser user = self.get();
     if (user instanceof AnonymousUser) {
       throw new AuthException("Authentication required");
-    } else if(!(user instanceof IdentifiedUser)) {
+    } else if(!(user.isIdentifiedUser())) {
       throw new ResourceNotFoundException();
     }
 
@@ -81,7 +80,7 @@
     final CurrentUser user = self.get();
     if (user instanceof AnonymousUser) {
       throw new AuthException("Authentication required");
-    } else if(!(user instanceof IdentifiedUser)) {
+    } else if(!(user.isIdentifiedUser())) {
       throw new ResourceNotFoundException(id);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
index a352cac..2ffff20 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.group.AddIncludedGroups.PutIncludedGroup;
 import com.google.gwtorm.server.OrmException;
@@ -78,8 +78,8 @@
 
   private boolean isMember(AccountGroup parent, GroupDescription.Basic member)
       throws OrmException {
-    return dbProvider.get().accountGroupIncludesByUuid().get(
-        new AccountGroupIncludeByUuid.Key(
+    return dbProvider.get().accountGroupById().get(
+        new AccountGroupById.Key(
             parent.getId(),
             member.getGroupUUID())) != null;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
index 3691e80..f112e34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
@@ -58,8 +58,8 @@
 
     boolean ownerOfParent = rsrc.getControl().isOwner();
     List<GroupInfo> included = Lists.newArrayList();
-    for (AccountGroupIncludeByUuid u : dbProvider.get()
-        .accountGroupIncludesByUuid()
+    for (AccountGroupById u : dbProvider.get()
+        .accountGroupById()
         .byGroup(rsrc.toAccountGroup().getId())) {
       try {
         GroupControl i = controlFactory.controlFor(u.getIncludeUUID());
@@ -67,7 +67,7 @@
           included.add(json.format(i.getGroup()));
         }
       } catch (NoSuchGroupException notFound) {
-        log.warn(String.format("Group %s no longer available, included into ",
+        log.warn(String.format("Group %s no longer available, included into %s",
             u.getIncludeUUID(),
             rsrc.getGroup().getName()));
         continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index d32c632..e150ceb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.server.account.AccountInfo;
 import com.google.gerrit.server.account.GroupCache;
@@ -49,7 +49,7 @@
   private boolean recursive;
 
   @Inject
-  ListMembers(GroupCache groupCache,
+  protected ListMembers(GroupCache groupCache,
       GroupDetailFactory.Factory groupDetailFactory,
       AccountInfo.Loader.Factory accountLoaderFactory) {
     this.groupCache = groupCache;
@@ -63,8 +63,19 @@
     if (resource.toAccountGroup() == null) {
       throw new MethodNotAllowedException();
     }
+
+    return apply(resource.getGroupUUID());
+  }
+
+  public List<AccountInfo> apply(AccountGroup group)
+      throws MethodNotAllowedException, OrmException {
+    return apply(group.getGroupUUID());
+  }
+
+  public List<AccountInfo> apply(AccountGroup.UUID groupId)
+      throws MethodNotAllowedException, OrmException {
     final Map<Account.Id, AccountInfo> members =
-        getMembers(resource.getGroupUUID(), new HashSet<AccountGroup.UUID>());
+        getMembers(groupId, new HashSet<AccountGroup.UUID>());
     final List<AccountInfo> memberInfos = Lists.newArrayList(members.values());
     Collections.sort(memberInfos, new Comparator<AccountInfo>() {
       @Override
@@ -108,7 +119,7 @@
 
     if (recursive) {
       if (groupDetail.includes != null) {
-        for (final AccountGroupIncludeByUuid includedGroup : groupDetail.includes) {
+        for (final AccountGroupById includedGroup : groupDetail.includes) {
           if (!seenGroups.contains(includedGroup.getIncludeUUID())) {
             members.putAll(getMembers(includedGroup.getIncludeUUID(), seenGroups));
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
new file mode 100644
index 0000000..7865d5e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
@@ -0,0 +1,378 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.patch.PatchListLoader;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.eclipse.jgit.util.io.NullOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ChangeBatchIndexer {
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeBatchIndexer.class);
+
+  public static class Result {
+    private final long elapsedNanos;
+    private final boolean success;
+    private final int done;
+    private final int failed;
+
+    private Result(Stopwatch sw, boolean success, int done, int failed) {
+      this.elapsedNanos = sw.elapsed(TimeUnit.NANOSECONDS);
+      this.success = success;
+      this.done = done;
+      this.failed = failed;
+    }
+
+    public boolean success() {
+      return success;
+    }
+
+    public int doneCount() {
+      return done;
+    }
+
+    public int failedCount() {
+      return failed;
+    }
+
+    public long elapsed(TimeUnit timeUnit) {
+      return timeUnit.convert(elapsedNanos, TimeUnit.NANOSECONDS);
+    }
+  }
+
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
+  private final ListeningExecutorService executor;
+  private final ChangeIndexer.Factory indexerFactory;
+
+  @Inject
+  ChangeBatchIndexer(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager,
+      @IndexExecutor ListeningExecutorService executor,
+      ChangeIndexer.Factory indexerFactory) {
+    this.db = db;
+    this.repoManager = repoManager;
+    this.executor = executor;
+    this.indexerFactory = indexerFactory;
+  }
+
+  public Result indexAll(ChangeIndex index, Iterable<Project.NameKey> projects,
+      int numProjects, int numChanges, OutputStream progressOut,
+      OutputStream verboseOut) {
+    if (progressOut == null) {
+      progressOut = NullOutputStream.INSTANCE;
+    }
+    PrintWriter verboseWriter = verboseOut != null ? new PrintWriter(verboseOut)
+        : null;
+
+    Stopwatch sw = Stopwatch.createStarted();
+    final MultiProgressMonitor mpm =
+        new MultiProgressMonitor(progressOut, "Reindexing changes");
+    final Task projTask = mpm.beginSubTask("projects",
+        numProjects >= 0 ? numProjects : MultiProgressMonitor.UNKNOWN);
+    final Task doneTask = mpm.beginSubTask(null,
+        numChanges >= 0 ? numChanges : MultiProgressMonitor.UNKNOWN);
+    final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+
+    final List<ListenableFuture<?>> futures = Lists.newArrayList();
+    final AtomicBoolean ok = new AtomicBoolean(true);
+
+    for (final Project.NameKey project : projects) {
+      final ListenableFuture<?> future = executor.submit(reindexProject(
+          indexerFactory.create(index), project, doneTask, failedTask,
+          verboseWriter));
+      futures.add(future);
+      future.addListener(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            future.get();
+          } catch (InterruptedException e) {
+            fail(project, e);
+          } catch (ExecutionException e) {
+            fail(project, e);
+          } catch (RuntimeException e) {
+            failAndThrow(project, e);
+          } catch (Error e) {
+            failAndThrow(project, e);
+          } finally {
+            projTask.update(1);
+          }
+        }
+
+        private void fail(Project.NameKey project, Throwable t) {
+          log.error("Failed to index project " + project, t);
+          ok.set(false);
+        }
+
+        private void failAndThrow(Project.NameKey project, RuntimeException e) {
+          fail(project, e);
+          throw e;
+        }
+
+        private void failAndThrow(Project.NameKey project, Error e) {
+          fail(project, e);
+          throw e;
+        }
+      }, MoreExecutors.sameThreadExecutor());
+    }
+
+    try {
+      mpm.waitFor(Futures.transform(Futures.successfulAsList(futures),
+          new AsyncFunction<List<?>, Void>() {
+            @Override
+            public ListenableFuture<Void> apply(List<?> input) {
+              mpm.end();
+              return Futures.immediateFuture(null);
+            }
+      }));
+    } catch (ExecutionException e) {
+      log.error("Error in batch indexer", e);
+      ok.set(false);
+    }
+    return new Result(sw, ok.get(), doneTask.getCount(), failedTask.getCount());
+  }
+
+  private Callable<Void> reindexProject(final ChangeIndexer indexer,
+      final Project.NameKey project, final Task done, final Task failed,
+      final PrintWriter verboseWriter) {
+    return new Callable<Void>() {
+      @Override
+      public Void call() throws Exception {
+        Multimap<ObjectId, ChangeData> byId = ArrayListMultimap.create();
+        Repository repo = repoManager.openRepository(project);
+        try {
+          Map<String, Ref> refs = repo.getRefDatabase().getRefs(ALL);
+          for (Change c : db.get().changes().byProject(project)) {
+            Ref r = refs.get(c.currentPatchSetId().toRefName());
+            if (r != null) {
+              byId.put(r.getObjectId(), new ChangeData(c));
+            }
+          }
+          new ProjectIndexer(indexer, byId, repo, done, failed, verboseWriter)
+              .call();
+        } finally {
+          repo.close();
+          // TODO(dborowitz): Opening all repositories in a live server may be
+          // wasteful; see if we can determine which ones it is safe to close
+          // with RepositoryCache.close(repo).
+        }
+        return null;
+      }
+    };
+  }
+
+  public static class ProjectIndexer implements Callable<Void> {
+    private final ChangeIndexer indexer;
+    private final Multimap<ObjectId, ChangeData> byId;
+    private final ProgressMonitor done;
+    private final ProgressMonitor failed;
+    private final PrintWriter verboseWriter;
+    private final Repository repo;
+    private RevWalk walk;
+
+    public ProjectIndexer(ChangeIndexer indexer,
+        Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo) {
+      this(indexer, changesByCommitId, repo,
+          NullProgressMonitor.INSTANCE, NullProgressMonitor.INSTANCE, null);
+    }
+
+    ProjectIndexer(ChangeIndexer indexer,
+        Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo,
+        ProgressMonitor done, ProgressMonitor failed, PrintWriter verboseWriter) {
+      this.indexer = indexer;
+      this.byId = changesByCommitId;
+      this.repo = repo;
+      this.done = done;
+      this.failed = failed;
+      this.verboseWriter = verboseWriter;
+    }
+
+    @Override
+    public Void call() throws Exception {
+      walk = new RevWalk(repo);
+      try {
+        // Walk only refs first to cover as many changes as we can without having
+        // to mark every single change.
+        for (Ref ref : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
+          RevObject o = walk.parseAny(ref.getObjectId());
+          if (o instanceof RevCommit) {
+            walk.markStart((RevCommit) o);
+          }
+        }
+
+        RevCommit bCommit;
+        while ((bCommit = walk.next()) != null && !byId.isEmpty()) {
+          if (byId.containsKey(bCommit)) {
+            getPathsAndIndex(bCommit);
+            byId.removeAll(bCommit);
+          }
+        }
+
+        for (ObjectId id : byId.keySet()) {
+          getPathsAndIndex(id);
+        }
+      } finally {
+        walk.release();
+      }
+      return null;
+    }
+
+    private void getPathsAndIndex(ObjectId b) throws Exception {
+      List<ChangeData> cds = Lists.newArrayList(byId.get(b));
+      try {
+        RevCommit bCommit = walk.parseCommit(b);
+        RevTree bTree = bCommit.getTree();
+        RevTree aTree = aFor(bCommit, walk);
+        DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+        try {
+          df.setRepository(repo);
+          if (!cds.isEmpty()) {
+            List<String> paths = (aTree != null)
+                ? getPaths(df.scan(aTree, bTree))
+                : Collections.<String>emptyList();
+            Iterator<ChangeData> cdit = cds.iterator();
+            for (ChangeData cd ; cdit.hasNext(); cdit.remove()) {
+              cd = cdit.next();
+              try {
+                cd.setCurrentFilePaths(paths);
+                indexer.index(cd);
+                done.update(1);
+                if (verboseWriter != null) {
+                  verboseWriter.println("Reindexed change " + cd.getId());
+                }
+              } catch (Exception e) {
+                fail("Failed to index change " + cd.getId(), true, e);
+              }
+            }
+          }
+        } finally {
+          df.release();
+        }
+      } catch (Exception e) {
+        fail("Failed to index commit " + b.name(), false, e);
+        for (ChangeData cd : cds) {
+          fail("Failed to index change " + cd.getId(), true, null);
+        }
+      }
+    }
+
+    private List<String> getPaths(List<DiffEntry> filenames) {
+      Set<String> paths = Sets.newTreeSet();
+      for (DiffEntry e : filenames) {
+        if (e.getOldPath() != null) {
+          paths.add(e.getOldPath());
+        }
+        if (e.getNewPath() != null) {
+          paths.add(e.getNewPath());
+        }
+      }
+      return ImmutableList.copyOf(paths);
+    }
+
+    private RevTree aFor(RevCommit b, RevWalk walk) throws IOException {
+      switch (b.getParentCount()) {
+        case 0:
+          return walk.parseTree(emptyTree());
+        case 1:
+          RevCommit a = b.getParent(0);
+          walk.parseBody(a);
+          return walk.parseTree(a.getTree());
+        case 2:
+          return PatchListLoader.automerge(repo, walk, b);
+        default:
+          return null;
+      }
+    }
+
+    private ObjectId emptyTree() throws IOException {
+      ObjectInserter oi = repo.newObjectInserter();
+      try {
+        ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
+        oi.flush();
+        return id;
+      } finally {
+        oi.release();
+      }
+    }
+
+    private void fail(String error, boolean failed, Exception e) {
+      if (failed) {
+        this.failed.update(1);
+      }
+
+      if (e != null) {
+        log.warn(error, e);
+      } else {
+        log.warn(error);
+      }
+
+      if (verboseWriter != null) {
+        verboseWriter.println(error);
+      }
+    }
+  }
+}
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
new file mode 100644
index 0000000..290bd31
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -0,0 +1,369 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+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.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.protobuf.CodedOutputStream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Fields indexed on change documents.
+ * <p>
+ * Each field corresponds to both a field name supported by
+ * {@link ChangeQueryBuilder} for querying that field, and a method on
+ * {@link ChangeData} used for populating the corresponding document fields in
+ * the secondary index.
+ */
+public class ChangeField {
+  /** Legacy change ID. */
+  public static final FieldDef<ChangeData, Integer> LEGACY_ID =
+      new FieldDef.Single<ChangeData, Integer>("_id",
+          FieldType.INTEGER, true) {
+        @Override
+        public Integer get(ChangeData input, FillArgs args) {
+          return input.getId().get();
+        }
+      };
+
+  /** Newer style Change-Id key. */
+  public static final FieldDef<ChangeData, String> ID =
+      new FieldDef.Single<ChangeData, String>("change_id",
+          FieldType.PREFIX, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change(args.db).getKey().get();
+        }
+      };
+
+  /** Change status string, in the same format as {@code status:}. */
+  public static final FieldDef<ChangeData, String> STATUS =
+      new FieldDef.Single<ChangeData, String>(ChangeQueryBuilder.FIELD_STATUS,
+          FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return ChangeStatusPredicate.VALUES.get(
+              input.change(args.db).getStatus());
+        }
+      };
+
+  /** Project containing the change. */
+  public static final FieldDef<ChangeData, String> PROJECT =
+      new FieldDef.Single<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_PROJECT, FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change(args.db).getProject().get();
+        }
+      };
+
+  /** Reference (aka branch) the change will submit onto. */
+  public static final FieldDef<ChangeData, String> REF =
+      new FieldDef.Single<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_REF, FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change(args.db).getDest().get();
+        }
+      };
+
+  /** Topic, a short annotation on the branch. */
+  public static final FieldDef<ChangeData, String> TOPIC =
+      new FieldDef.Single<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_TOPIC, FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change(args.db).getTopic();
+        }
+      };
+
+  /** Last update time since January 1, 1970. */
+  public static final FieldDef<ChangeData, Timestamp> UPDATED =
+      new FieldDef.Single<ChangeData, Timestamp>(
+          "updated", FieldType.TIMESTAMP, true) {
+        @Override
+        public Timestamp get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change(args.db).getLastUpdatedOn();
+        }
+      };
+
+  @Deprecated
+  public static long legacyParseSortKey(String sortKey) {
+    if ("z".equals(sortKey)) {
+      return Long.MAX_VALUE;
+    }
+    return Long.parseLong(sortKey.substring(0, 8), 16);
+  }
+
+  /** Legacy sort key field. */
+  @Deprecated
+  public static final FieldDef<ChangeData, Long> LEGACY_SORTKEY =
+      new FieldDef.Single<ChangeData, Long>(
+          "sortkey", FieldType.LONG, true) {
+        @Override
+        public Long get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return legacyParseSortKey(input.change(args.db).getSortKey());
+        }
+      };
+
+  /**
+   * Sort key field.
+   * <p>
+   * Redundant with {@link #UPDATED} and {@link #LEGACY_ID}, but secondary index
+   * implementations may not be able to search over tuples of values.
+   */
+  public static final FieldDef<ChangeData, Long> SORTKEY =
+      new FieldDef.Single<ChangeData, Long>(
+          "sortkey2", FieldType.LONG, true) {
+        @Override
+        public Long get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return ChangeUtil.parseSortKey(input.change(args.db).getSortKey());
+        }
+      };
+
+  /** List of filenames modified in the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> FILE =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_FILE, FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.currentFilePaths(args.db, args.patchListCache);
+        }
+      };
+
+  /** Owner/creator of the change. */
+  public static final FieldDef<ChangeData, Integer> OWNER =
+      new FieldDef.Single<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_OWNER, FieldType.INTEGER, false) {
+        @Override
+        public Integer get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change(args.db).getOwner().get();
+        }
+      };
+
+  /** Reviewer(s) associated with the change. */
+  public static final FieldDef<ChangeData, Iterable<Integer>> REVIEWER =
+      new FieldDef.Repeatable<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_REVIEWER, FieldType.INTEGER, false) {
+        @Override
+        public Iterable<Integer> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<Integer> r = Sets.newHashSet();
+          for (PatchSetApproval a : input.allApprovals(args.db)) {
+            r.add(a.getAccountId().get());
+          }
+          return r;
+        }
+      };
+
+  /** Commit id of any PatchSet on the change */
+  public static final FieldDef<ChangeData, Iterable<String>> COMMIT =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_COMMIT, FieldType.PREFIX, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<String> revisions = Sets.newHashSet();
+          for (PatchSet ps : input.patches(args.db)) {
+            if (ps.getRevision() != null) {
+              revisions.add(ps.getRevision().get());
+            }
+          }
+          return revisions;
+        }
+      };
+
+  /** Tracking id extracted from a footer. */
+  public static final FieldDef<ChangeData, Iterable<String>> TR =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_TR, FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          try {
+            return args.trackingFooters.extract(
+                input.commitFooters(args.repoManager, args.db));
+          } catch (IOException e) {
+            throw new OrmException(e);
+          }
+        }
+      };
+
+  /** List of labels on the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> LABEL =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_LABEL, FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<String> allApprovals = Sets.newHashSet();
+          Set<String> distinctApprovals = Sets.newHashSet();
+          for (PatchSetApproval a : input.currentApprovals(args.db)) {
+            if (a.getValue() != 0) {
+              allApprovals.add(formatLabel(a.getLabel(), a.getValue(),
+                  a.getAccountId()));
+              distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
+            }
+          }
+          allApprovals.addAll(distinctApprovals);
+          return allApprovals;
+        }
+      };
+
+  /** Set true if the change has a non-zero label score. */
+  public static final FieldDef<ChangeData, String> REVIEWED =
+      new FieldDef.Single<ChangeData, String>(
+          "reviewed", FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          for (PatchSetApproval a : input.currentApprovals(args.db)) {
+            if (a.getValue() != 0) {
+              return "1";
+            }
+          }
+          return null;
+        }
+      };
+
+  public static class ChangeProtoField extends FieldDef.Single<ChangeData, byte[]> {
+    public static final ProtobufCodec<Change> CODEC =
+        CodecFactory.encoder(Change.class);
+
+    private ChangeProtoField() {
+      super("_change", FieldType.STORED_ONLY, true);
+    }
+
+    @Override
+    public byte[] get(ChangeData input, FieldDef.FillArgs args)
+        throws OrmException {
+      return CODEC.encodeToByteArray(input.change(args.db));
+    }
+  }
+
+  /** Serialized change object, used for pre-populating results. */
+  public static final ChangeProtoField CHANGE = new ChangeProtoField();
+
+  public static class PatchSetApprovalProtoField
+      extends FieldDef.Repeatable<ChangeData, byte[]> {
+    public static final ProtobufCodec<PatchSetApproval> CODEC =
+        CodecFactory.encoder(PatchSetApproval.class);
+
+    private PatchSetApprovalProtoField() {
+      super("_approval", FieldType.STORED_ONLY, true);
+    }
+
+    @Override
+    public Iterable<byte[]> get(ChangeData input, FillArgs args)
+        throws OrmException {
+      return toProtos(CODEC, input.currentApprovals(args.db));
+    }
+  }
+
+  /**
+   * Serialized approvals for the current patch set, used for pre-populating
+   * results.
+   */
+  public static final PatchSetApprovalProtoField APPROVAL =
+      new PatchSetApprovalProtoField();
+
+  public static String formatLabel(String label, int value) {
+    return formatLabel(label, value, null);
+  }
+
+  public static String formatLabel(String label, int value, Account.Id accountId) {
+    return label.toLowerCase() + (value >= 0 ? "+" : "") + value
+        + (accountId != null ? "," + accountId.get() : "");
+  }
+
+  /** Commit message of the current patch set. */
+  public static final FieldDef<ChangeData, String> COMMIT_MESSAGE =
+      new FieldDef.Single<ChangeData, String>(ChangeQueryBuilder.FIELD_MESSAGE,
+          FieldType.FULL_TEXT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args) throws OrmException {
+          try {
+            return input.commitMessage(args.repoManager, args.db);
+          } catch (IOException e) {
+            throw new OrmException(e);
+          }
+        }
+      };
+
+  /** Summary or inline comment. */
+  public static final FieldDef<ChangeData, Iterable<String>> COMMENT =
+      new FieldDef.Repeatable<ChangeData, String>(ChangeQueryBuilder.FIELD_COMMENT,
+          FieldType.FULL_TEXT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<String> r = Sets.newHashSet();
+          for (PatchLineComment c : input.comments(args.db)) {
+            r.add(c.getMessage());
+          }
+          for (ChangeMessage m : input.messages(args.db)) {
+            r.add(m.getMessage());
+          }
+          return r;
+        }
+      };
+
+  private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
+      throws OrmException {
+    List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
+    ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+    try {
+      for (T obj : objs) {
+        out.reset();
+        CodedOutputStream cos = CodedOutputStream.newInstance(out);
+        codec.encode(obj, cos);
+        cos.flush();
+        result.add(out.toByteArray());
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+    return result;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
new file mode 100644
index 0000000..397b044
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -0,0 +1,157 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+
+import java.io.IOException;
+
+/**
+ * Secondary index implementation for change documents.
+ * <p>
+ * {@link ChangeData} objects are inserted into the index and are queried by
+ * converting special {@link com.google.gerrit.server.query.Predicate} instances
+ * into index-aware predicates that use the index search results as a source.
+ * <p>
+ * Implementations must be thread-safe and should batch inserts/updates where
+ * appropriate.
+ */
+public interface ChangeIndex {
+  /** Instance indicating secondary index is disabled. */
+  public static final ChangeIndex DISABLED = new ChangeIndex() {
+    @Override
+    public Schema<ChangeData> getSchema() {
+      return null;
+    }
+
+    @Override
+    public void insert(ChangeData cd) throws IOException {
+      // Do nothing.
+    }
+
+    @Override
+    public void replace(ChangeData cd) throws IOException {
+      // Do nothing.
+    }
+
+    @Override
+    public void delete(ChangeData cd) throws IOException {
+      // Do nothing.
+    }
+
+    @Override
+    public void deleteAll() throws IOException {
+      // Do nothing.
+    }
+
+    @Override
+    public ChangeDataSource getSource(Predicate<ChangeData> p, int limit) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void close() {
+      // Do nothing.
+    }
+
+    @Override
+    public void markReady(boolean ready) {
+      throw new UnsupportedOperationException();
+    }
+  };
+
+  /** @return the schema version used by this index. */
+  public Schema<ChangeData> getSchema();
+
+  /** Close this index. */
+  public void close();
+
+  /**
+   * Insert a change document into the index.
+   * <p>
+   * Results may not be immediately visible to searchers, but should be visible
+   * within a reasonable amount of time.
+   *
+   * @param cd change document
+   *
+   * @throws IOException if the change could not be inserted.
+   */
+  public void insert(ChangeData cd) throws IOException;
+
+  /**
+   * Update a change document in the index.
+   * <p>
+   * Semantically equivalent to deleting the document and reinserting it with
+   * new field values. Results may not be immediately visible to searchers, but
+   * should be visible within a reasonable amount of time.
+   *
+   * @param cd change document
+   *
+   * @throws IOException
+   */
+  public void replace(ChangeData cd) throws IOException;
+
+  /**
+   * Delete a change document from the index.
+   *
+   * @param cd change document.
+   *
+   * @throws IOException
+   */
+  public void delete(ChangeData cd) throws IOException;
+
+  /**
+   * Delete all change documents from the index.
+   *
+   * @throws IOException
+   */
+  public void deleteAll() throws IOException;
+
+  /**
+   * Convert the given operator predicate into a source searching the index and
+   * returning only the documents matching that predicate.
+   * <p>
+   * This method may be called multiple times for variations on the same
+   * predicate or multiple predicate subtrees in the course of processing a
+   * single query, so it should not have any side effects (e.g. starting a
+   * search in the background).
+   *
+   * @param p the predicate to match. Must be a tree containing only AND, OR,
+   *     or NOT predicates as internal nodes, and {@link IndexPredicate}s as
+   *     leaves.
+   * @param limit maximum number of results to return.
+   * @return a source of documents matching the predicate. Documents must be
+   *     returned in descending sort key order, unless a {@code sortkey_after}
+   *     predicate (with a cut point not at {@link Long#MAX_VALUE}) is provided,
+   *     in which case the source should return documents in ascending sort key
+   *     order starting from the sort key cut point.
+   *
+   * @throws QueryParseException if the predicate could not be converted to an
+   *     indexed data source.
+   */
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+      throws QueryParseException;
+
+  /**
+   * Mark whether this index is up-to-date and ready to serve reads.
+   *
+   * @param ready whether the index is ready
+   * @throws IOException
+   */
+  public void markReady(boolean ready) throws IOException;
+}
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
new file mode 100644
index 0000000..ac9373d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -0,0 +1,191 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Callables;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Helper for (re)indexing a change document.
+ * <p>
+ * Indexing is run in the background, as it may require substantial work to
+ * compute some of the fields and/or update the index.
+ */
+public abstract class ChangeIndexer {
+  public interface Factory {
+    ChangeIndexer create(ChangeIndex index);
+    ChangeIndexer create(IndexCollection indexes);
+  }
+
+  /** Instance indicating secondary index is disabled. */
+  public static final ChangeIndexer DISABLED = new ChangeIndexer(null) {
+    @Override
+    public CheckedFuture<?, IOException> indexAsync(ChangeData cd) {
+      return Futures.immediateCheckedFuture(null);
+    }
+
+    @Override
+    protected Callable<?> indexTask(ChangeData cd) {
+      return Callables.returning(null);
+    }
+
+    @Override
+    protected Callable<?> deleteTask(ChangeData cd) {
+      return Callables.returning(null);
+    }
+  };
+
+  private static final Function<Exception, IOException> MAPPER =
+      new Function<Exception, IOException>() {
+    @Override
+    public IOException apply(Exception in) {
+      if (in instanceof IOException) {
+        return (IOException) in;
+      } else if (in instanceof ExecutionException
+          && in.getCause() instanceof IOException) {
+        return (IOException) in.getCause();
+      } else {
+        return new IOException(in);
+      }
+    }
+  };
+
+  private final ListeningExecutorService executor;
+
+  protected ChangeIndexer(ListeningExecutorService executor) {
+    this.executor = executor;
+  }
+
+  /**
+   * Start indexing a change.
+   *
+   * @param change change to index.
+   * @return future for the indexing task.
+   */
+  public CheckedFuture<?, IOException> indexAsync(Change change) {
+    return indexAsync(new ChangeData(change));
+  }
+
+  /**
+   * Start indexing a change.
+   *
+   * @param cd change to index.
+   * @return future for the indexing task.
+   */
+  public CheckedFuture<?, IOException> indexAsync(ChangeData cd) {
+    return executor != null
+        ? submit(indexTask(cd))
+        : Futures.<Object, IOException> immediateCheckedFuture(null);
+  }
+
+  /**
+   * Synchronously index a change.
+   *
+   * @param change change to index.
+   */
+  public void index(Change change) throws IOException {
+    index(new ChangeData(change));
+  }
+
+  /**
+   * Synchronously index a change.
+   *
+   * @param cd change to index.
+   */
+  public void index(ChangeData cd) throws IOException {
+    try {
+      indexTask(cd).call();
+    } catch (RuntimeException e) {
+      throw e;
+    } catch (Exception e) {
+      throw MAPPER.apply(e);
+    }
+  }
+
+  /**
+   * Create a runnable to index a change.
+   *
+   * @param cd change to index.
+   * @return unstarted runnable to index the change.
+   */
+  protected abstract Callable<?> indexTask(ChangeData cd);
+
+  /**
+   * Start deleting a change.
+   *
+   * @param change change to delete.
+   * @return future for the deleting task.
+   */
+  public CheckedFuture<?, IOException> deleteAsync(Change change) {
+    return deleteAsync(new ChangeData(change));
+  }
+
+  /**
+   * Start deleting a change.
+   *
+   * @param cd change to delete.
+   * @return future for the deleting task.
+   */
+  public CheckedFuture<?, IOException> deleteAsync(ChangeData cd) {
+    return executor != null
+        ? submit(deleteTask(cd))
+        : Futures.<Object, IOException> immediateCheckedFuture(null);
+  }
+
+  /**
+   * Synchronously delete a change.
+   *
+   * @param change change to delete.
+   */
+  public void delete(Change change) throws IOException {
+    delete(new ChangeData(change));
+  }
+
+  /**
+   * Synchronously delete a change.
+   *
+   * @param cd change to delete.
+   */
+  public void delete(ChangeData cd) throws IOException {
+    try {
+      deleteTask(cd).call();
+    } catch (RuntimeException e) {
+      throw e;
+    } catch (Exception e) {
+      throw MAPPER.apply(e);
+    }
+  }
+
+  /**
+   * Create a runnable to delete a change.
+   *
+   * @param cd change to delete.
+   * @return unstarted runnable to delete the change.
+   */
+  protected abstract Callable<?> deleteTask(ChangeData cd);
+
+  private CheckedFuture<?, IOException> submit(Callable<?> task) {
+    return Futures.makeChecked(executor.submit(task), MAPPER);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
new file mode 100644
index 0000000..93d3f9f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.util.concurrent.Atomics;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.google.inject.util.Providers;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper for (re)indexing a change document.
+ * <p>
+ * Indexing is run in the background, as it may require substantial work to
+ * compute some of the fields and/or update the index.
+ */
+public class ChangeIndexerImpl extends ChangeIndexer {
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeIndexerImpl.class);
+
+  private final IndexCollection indexes;
+  private final ChangeIndex index;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ThreadLocalRequestContext context;
+
+  @AssistedInject
+  ChangeIndexerImpl(@IndexExecutor ListeningExecutorService executor,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ThreadLocalRequestContext context,
+      @Assisted ChangeIndex index) {
+    super(executor);
+    this.schemaFactory = schemaFactory;
+    this.context = context;
+    this.index = index;
+    this.indexes = null;
+  }
+
+  @AssistedInject
+  ChangeIndexerImpl(@IndexExecutor ListeningExecutorService executor,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ThreadLocalRequestContext context,
+      @Assisted IndexCollection indexes) {
+    super(executor);
+    this.schemaFactory = schemaFactory;
+    this.context = context;
+    this.index = null;
+    this.indexes = indexes;
+  }
+
+  @Override
+  protected Callable<Void> indexTask(ChangeData cd) {
+    return new Task(cd, false);
+  }
+
+  @Override
+  protected Callable<Void> deleteTask(ChangeData cd) {
+    return new Task(cd, true);
+  }
+
+  private class Task implements Callable<Void> {
+    private final ChangeData cd;
+    private final boolean delete;
+
+    private Task(ChangeData cd, boolean delete) {
+      this.cd = cd;
+      this.delete = delete;
+    }
+
+    @Override
+    public Void call() throws Exception {
+      try {
+        final AtomicReference<Provider<ReviewDb>> dbRef =
+            Atomics.newReference();
+        RequestContext oldCtx = context.setContext(new RequestContext() {
+          @Override
+          public Provider<ReviewDb> getReviewDbProvider() {
+            Provider<ReviewDb> db = dbRef.get();
+            if (db == null) {
+              try {
+                db = Providers.of(schemaFactory.open());
+              } catch (OrmException e) {
+                ProvisionException pe =
+                    new ProvisionException("error opening ReviewDb");
+                pe.initCause(e);
+                throw pe;
+              }
+              dbRef.set(db);
+            }
+            return db;
+          }
+
+          @Override
+          public CurrentUser getCurrentUser() {
+            throw new OutOfScopeException("No user during ChangeIndexer");
+          }
+        });
+        try {
+          if (indexes != null) {
+            for (ChangeIndex i : indexes.getWriteIndexes()) {
+              apply(i, cd);
+            }
+          } else {
+            apply(index, cd);
+          }
+          return null;
+        } finally  {
+          context.setContext(oldCtx);
+          Provider<ReviewDb> db = dbRef.get();
+          if (db != null) {
+            db.get().close();
+          }
+        }
+      } catch (Exception e) {
+        log.error(String.format(
+            "Failed to index change %d in %s",
+            cd.getId().get(), cd.getChange().getProject().get()), e);
+        throw e;
+      }
+    }
+
+    private void apply(ChangeIndex i, ChangeData cd) throws IOException {
+      if (delete) {
+        i.delete(cd);
+      } else {
+        i.replace(cd);
+      }
+    }
+
+    @Override
+    public String toString() {
+      return "index-change-" + cd.getId().get();
+    }
+  }
+}
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
new file mode 100644
index 0000000..0654e80
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+/** Secondary index schemas for changes. */
+public class ChangeSchemas {
+  @SuppressWarnings({"unchecked", "deprecation"})
+  static final Schema<ChangeData> V1 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.REF,
+        ChangeField.TOPIC,
+        ChangeField.UPDATED,
+        ChangeField.LEGACY_SORTKEY,
+        ChangeField.FILE,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT);
+
+  @SuppressWarnings({"unchecked", "deprecation"})
+  static final Schema<ChangeData> V2 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.REF,
+        ChangeField.TOPIC,
+        ChangeField.UPDATED,
+        ChangeField.LEGACY_SORTKEY,
+        ChangeField.FILE,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT,
+        ChangeField.CHANGE,
+        ChangeField.APPROVAL);
+
+  @SuppressWarnings("unchecked")
+  static final Schema<ChangeData> V3 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.REF,
+        ChangeField.TOPIC,
+        ChangeField.UPDATED,
+        ChangeField.SORTKEY,
+        ChangeField.FILE,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT,
+        ChangeField.CHANGE,
+        ChangeField.APPROVAL);
+
+  // For upgrade to Lucene 4.4.0 index format only.
+  static final Schema<ChangeData> V4 = release(V3.getFields().values());
+
+  private static Schema<ChangeData> release(Collection<FieldDef<ChangeData, ?>> fields) {
+    return new Schema<ChangeData>(true, fields);
+  }
+
+  private static Schema<ChangeData> release(FieldDef<ChangeData, ?>... fields) {
+    return release(Arrays.asList(fields));
+  }
+
+  @SuppressWarnings("unused")
+  private static Schema<ChangeData> developer(FieldDef<ChangeData, ?>... fields) {
+    return new Schema<ChangeData>(false, Arrays.asList(fields));
+  }
+
+  public static final ImmutableMap<Integer, Schema<ChangeData>> ALL;
+
+  public static Schema<ChangeData> get(int version) {
+    Schema<ChangeData> schema = ALL.get(version);
+    checkArgument(schema != null, "Unrecognized schema version: %s", version);
+    return schema;
+  }
+
+  public static Schema<ChangeData> getLatest() {
+    return Iterables.getLast(ALL.values());
+  }
+
+  static {
+    Map<Integer, Schema<ChangeData>> all = Maps.newTreeMap();
+    for (Field f : ChangeSchemas.class.getDeclaredFields()) {
+      if (Modifier.isStatic(f.getModifiers())
+          && Modifier.isFinal(f.getModifiers())
+          && Schema.class.isAssignableFrom(f.getType())) {
+        ParameterizedType t = (ParameterizedType) f.getGenericType();
+        if (t.getActualTypeArguments()[0] == ChangeData.class) {
+          try {
+            @SuppressWarnings("unchecked")
+            Schema<ChangeData> schema = (Schema<ChangeData>) f.get(null);
+            checkArgument(f.getName().startsWith("V"));
+            schema.setVersion(Integer.parseInt(f.getName().substring(1)));
+            all.put(schema.getVersion(), schema);
+          } catch (IllegalArgumentException e) {
+            throw new ExceptionInInitializerError(e);
+          } catch (IllegalAccessException e) {
+            throw new ExceptionInInitializerError(e);
+          }
+        } else {
+          throw new ExceptionInInitializerError(
+              "non-ChangeData schema: " + f);
+        }
+      }
+    }
+    if (all.isEmpty()) {
+      throw new ExceptionInInitializerError("no ChangeSchemas found");
+    }
+    ALL = ImmutableMap.copyOf(all);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
new file mode 100644
index 0000000..7bec5a5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Definition of a field stored in the secondary index.
+ *
+ * @param <I> input type from which documents are created and search results are
+ *     returned.
+ * @param <T> type that should be extracted from the input object when converting
+ *     to an index document.
+ */
+public abstract class FieldDef<I, T> {
+  /** Definition of a single (non-repeatable) field. */
+  public static abstract class Single<I, T> extends FieldDef<I, T> {
+    Single(String name, FieldType<T> type, boolean stored) {
+      super(name, type, stored);
+    }
+
+    @Override
+    public final boolean isRepeatable() {
+      return false;
+    }
+  }
+
+  /** Definition of a repeatable field. */
+  public static abstract class Repeatable<I, T>
+      extends FieldDef<I, Iterable<T>> {
+    Repeatable(String name, FieldType<T> type, boolean stored) {
+      super(name, type, stored);
+    }
+
+    @Override
+    public final boolean isRepeatable() {
+      return true;
+    }
+  }
+
+  /** Arguments needed to fill in missing data in the input object. */
+  public static class FillArgs {
+    final Provider<ReviewDb> db;
+    final GitRepositoryManager repoManager;
+    final TrackingFooters trackingFooters;
+    final PatchListCache patchListCache;
+
+    @Inject
+    FillArgs(Provider<ReviewDb> db,
+        GitRepositoryManager repoManager,
+        TrackingFooters trackingFooters,
+        PatchListCache patchListCache) {
+      this.db = db;
+      this.repoManager = repoManager;
+      this.trackingFooters = trackingFooters;
+      this.patchListCache = patchListCache;
+    }
+  }
+
+  private final String name;
+  private final FieldType<?> type;
+  private final boolean stored;
+
+  private FieldDef(String name, FieldType<?> type, boolean stored) {
+    this.name = name;
+    this.type = type;
+    this.stored = stored;
+  }
+
+  /** @return name of the field. */
+  public final String getName() {
+    return name;
+  }
+
+  /**
+   * @return type of the field; for repeatable fields, the inner type, not the
+   *     iterable type.
+   */
+  public final FieldType<?> getType() {
+    return type;
+  }
+
+  /** @return whether the field should be stored in the index. */
+  public final boolean isStored() {
+    return stored;
+  }
+
+  /**
+   * Get the field contents from the input object.
+   *
+   * @param input input object.
+   * @param args arbitrary arguments needed to fill in indexable fields of the
+   *     input object.
+   * @return the field value(s) to index.
+   *
+   * @throws OrmException
+   */
+  public abstract T get(I input, FillArgs args) throws OrmException;
+
+  /** @return whether the field is repeatable. */
+  public abstract boolean isRepeatable();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
new file mode 100644
index 0000000..a3247b9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import java.sql.Timestamp;
+
+
+/** Document field types supported by the secondary index system. */
+public class FieldType<T> {
+  /** A single integer-valued field. */
+  public static final FieldType<Integer> INTEGER =
+      new FieldType<Integer>("INTEGER");
+
+  /** A single integer-valued field. */
+  public static final FieldType<Long> LONG =
+      new FieldType<Long>("LONG");
+
+  /** A single date/time-valued field. */
+  public static final FieldType<Timestamp> TIMESTAMP =
+      new FieldType<Timestamp>("TIMESTAMP");
+
+  /** A string field searched using exact-match semantics. */
+  public static final FieldType<String> EXACT =
+      new FieldType<String>("EXACT");
+
+  /** A string field searched using prefix. */
+  public static final FieldType<String> PREFIX =
+      new FieldType<String>("PREFIX");
+
+  /** A string field searched using fuzzy-match semantics. */
+  public static final FieldType<String> FULL_TEXT =
+      new FieldType<String>("FULL_TEXT");
+
+  /** A field that is only stored as raw bytes and cannot be queried. */
+  public static final FieldType<byte[]> STORED_ONLY =
+      new FieldType<byte[]>("STORED_ONLY");
+
+  private final String name;
+
+  private FieldType(String name) {
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
new file mode 100644
index 0000000..0c90e32
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Dynamic pointers to the index versions used for searching and writing. */
+@Singleton
+public class IndexCollection implements LifecycleListener {
+  private final CopyOnWriteArrayList<ChangeIndex> writeIndexes;
+  private final AtomicReference<ChangeIndex> searchIndex;
+
+  @Inject
+  @VisibleForTesting
+  public IndexCollection() {
+    this.writeIndexes = Lists.newCopyOnWriteArrayList();
+    this.searchIndex = new AtomicReference<ChangeIndex>();
+  }
+
+  /**
+   * @return the current search index version, or null if the secondary index is
+   *     disabled.
+   */
+  @Nullable
+  public ChangeIndex getSearchIndex() {
+    return searchIndex.get();
+  }
+
+  public void setSearchIndex(ChangeIndex index) {
+    ChangeIndex old = searchIndex.getAndSet(index);
+    if (old != null && old != index) {
+      old.close();
+    }
+  }
+
+  public Collection<ChangeIndex> getWriteIndexes() {
+    return Collections.unmodifiableCollection(writeIndexes);
+  }
+
+  public synchronized ChangeIndex addWriteIndex(ChangeIndex index) {
+    int version = index.getSchema().getVersion();
+    for (int i = 0; i < writeIndexes.size(); i++) {
+      if (writeIndexes.get(i).getSchema().getVersion() == version) {
+        return writeIndexes.set(i, index);
+      }
+    }
+    writeIndexes.add(index);
+    return null;
+  }
+
+  public synchronized void removeWriteIndex(int version) {
+    int removeIndex = -1;
+    for (int i = 0; i < writeIndexes.size(); i++) {
+      if (writeIndexes.get(i).getSchema().getVersion() == version) {
+        removeIndex = i;
+        break;
+      }
+    }
+    if (removeIndex >= 0) {
+      try {
+        writeIndexes.get(removeIndex).close();
+      } finally {
+        writeIndexes.remove(removeIndex);
+      }
+    }
+  }
+
+  public ChangeIndex getWriteIndex(int version) {
+    for (ChangeIndex i : writeIndexes) {
+      if (i.getSchema().getVersion() == version) {
+        return i;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public void start() {
+  }
+
+  @Override
+  public void stop() {
+    ChangeIndex read = searchIndex.get();
+    if (read != null) {
+      read.close();
+    }
+    for (ChangeIndex write : writeIndexes) {
+      if (write != read) {
+        write.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
new file mode 100644
index 0000000..0a96d1d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Marker on {@link ListeningExecutorService} used by secondary indexing
+ * threads.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface IndexExecutor {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
new file mode 100644
index 0000000..2cfc93d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Module for non-indexer-specific secondary index setup.
+ * <p>
+ * This module should not be used directly except by specific secondary indexer
+ * implementations (e.g. Lucene).
+ */
+public class IndexModule extends LifecycleModule {
+  public enum IndexType {
+    SQL, LUCENE, SOLR;
+  }
+
+  /** Type of secondary index. */
+  public static IndexType getIndexType(Injector injector) {
+    Config cfg = injector.getInstance(
+        Key.get(Config.class, GerritServerConfig.class));
+    return cfg.getEnum("index", null, "type", IndexType.SQL);
+  }
+
+  private final int threads;
+  private final ListeningExecutorService indexExecutor;
+
+  public IndexModule(int threads) {
+    this.threads = threads;
+    this.indexExecutor = null;
+  }
+
+  public IndexModule(ListeningExecutorService indexExecutor) {
+    this.threads = -1;
+    this.indexExecutor = indexExecutor;
+  }
+
+  @Override
+  protected void configure() {
+    bind(ChangeQueryRewriter.class).to(IndexRewriteImpl.class);
+    bind(IndexRewriteImpl.BasicRewritesImpl.class);
+    bind(IndexCollection.class);
+    listener().to(IndexCollection.class);
+    install(new FactoryModuleBuilder()
+        .implement(ChangeIndexer.class, ChangeIndexerImpl.class)
+        .build(ChangeIndexer.Factory.class));
+
+    if (indexExecutor != null) {
+      bind(ListeningExecutorService.class)
+          .annotatedWith(IndexExecutor.class)
+          .toInstance(indexExecutor);
+    } else {
+      install(new IndexExecutorModule(threads));
+    }
+  }
+
+  @Provides
+  ChangeIndexer getChangeIndexer(
+      ChangeIndexer.Factory factory,
+      IndexCollection indexes) {
+    return factory.create(indexes);
+  }
+
+  private static class IndexExecutorModule extends AbstractModule {
+    private final int threads;
+
+    private IndexExecutorModule(int threads) {
+      this.threads = threads;
+    }
+
+    @Override
+    public void configure() {
+    }
+
+    @Provides
+    @Singleton
+    @IndexExecutor
+    ListeningExecutorService getIndexExecutor(
+        @GerritServerConfig Config config,
+        WorkQueue workQueue) {
+      int threads = this.threads;
+      if (threads <= 0) {
+        threads = config.getInt("index", null, "threads", 0);
+      }
+      if (threads <= 0) {
+        return MoreExecutors.sameThreadExecutor();
+      }
+      return MoreExecutors.listeningDecorator(
+          workQueue.createQueue(threads, "index"));
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
new file mode 100644
index 0000000..d3b9e95
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.OperatorPredicate;
+
+/** Index-aware predicate that includes a field type annotation. */
+public abstract class IndexPredicate<I> extends OperatorPredicate<I> {
+  private final FieldDef<I, ?> def;
+
+  public IndexPredicate(FieldDef<I, ?> def, String value) {
+    super(def.getName(), value);
+    this.def = def;
+  }
+
+  protected IndexPredicate(FieldDef<I, ?> def, String name, String value) {
+    super(name, value);
+    this.def = def;
+  }
+
+  public FieldDef<I, ?> getField() {
+    return def;
+  }
+
+  public FieldType<?> getType() {
+    return def.getType();
+  }
+}
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
new file mode 100644
index 0000000..414715d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
@@ -0,0 +1,284 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.gerrit.server.query.change.AndSource;
+import com.google.gerrit.server.query.change.BasicChangeRewrites;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.BitSet;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/** Rewriter that pushes boolean logic into the secondary index. */
+public class IndexRewriteImpl implements ChangeQueryRewriter {
+  /** Set of all open change statuses. */
+  public static final Set<Change.Status> OPEN_STATUSES;
+
+  /** Set of all closed change statuses. */
+  public static final Set<Change.Status> CLOSED_STATUSES;
+
+  static {
+    EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
+    EnumSet<Change.Status> closed = EnumSet.noneOf(Change.Status.class);
+    for (Change.Status s : Change.Status.values()) {
+      if (s.isOpen()) {
+        open.add(s);
+      } else {
+        closed.add(s);
+      }
+    }
+    OPEN_STATUSES = Sets.immutableEnumSet(open);
+    CLOSED_STATUSES = Sets.immutableEnumSet(closed);
+  }
+
+  @VisibleForTesting
+  static final int MAX_LIMIT = 1000;
+
+  /**
+   * Get the set of statuses that changes matching the given predicate may have.
+   *
+   * @param in predicate
+   * @return the maximal set of statuses that any changes matching the input
+   *     predicates may have, based on examining boolean and
+   *     {@link ChangeStatusPredicate}s.
+   */
+  public static EnumSet<Change.Status> getPossibleStatus(Predicate<ChangeData> in) {
+    EnumSet<Change.Status> s = extractStatus(in);
+    return s != null ? s : EnumSet.allOf(Change.Status.class);
+  }
+
+  private static EnumSet<Change.Status> extractStatus(Predicate<ChangeData> in) {
+    if (in instanceof ChangeStatusPredicate) {
+      return EnumSet.of(((ChangeStatusPredicate) in).getStatus());
+    } else if (in instanceof NotPredicate) {
+      EnumSet<Status> s = extractStatus(in.getChild(0));
+      return s != null ? EnumSet.complementOf(s) : null;
+    } else if (in instanceof OrPredicate) {
+      EnumSet<Change.Status> r = null;
+      int childrenWithStatus = 0;
+      for (int i = 0; i < in.getChildCount(); i++) {
+        EnumSet<Status> c = extractStatus(in.getChild(i));
+        if (c != null) {
+          if (r == null) {
+            r = EnumSet.noneOf(Change.Status.class);
+          }
+          r.addAll(c);
+          childrenWithStatus++;
+        }
+      }
+      if (r != null && childrenWithStatus < in.getChildCount()) {
+        // At least one child supplied a status but another did not.
+        // Assume all statuses for the children that did not feed a
+        // status at this part of the tree. This matches behavior if
+        // the child was used at the root of a query.
+        return EnumSet.allOf(Change.Status.class);
+      }
+      return r;
+    } else if (in instanceof AndPredicate) {
+      EnumSet<Change.Status> r = null;
+      for (int i = 0; i < in.getChildCount(); i++) {
+        EnumSet<Change.Status> c = extractStatus(in.getChild(i));
+        if (c != null) {
+          if (r == null) {
+            r = EnumSet.allOf(Change.Status.class);
+          }
+          r.retainAll(c);
+        }
+      }
+      return r;
+    }
+    return null;
+  }
+
+  private final IndexCollection indexes;
+  private final Provider<ReviewDb> db;
+  private final BasicRewritesImpl basicRewrites;
+  private final SqlRewriterImpl sqlRewriter;
+
+  @Inject
+  IndexRewriteImpl(IndexCollection indexes,
+      Provider<ReviewDb> db,
+      BasicRewritesImpl basicRewrites,
+      SqlRewriterImpl sqlRewriter) {
+    this.indexes = indexes;
+    this.db = db;
+    this.basicRewrites = basicRewrites;
+    this.sqlRewriter = sqlRewriter;
+  }
+
+  @Override
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+      throws QueryParseException {
+    ChangeIndex index = indexes.getSearchIndex();
+    if (index == null) {
+      return sqlRewriter.rewrite(in);
+    }
+    in = basicRewrites.rewrite(in);
+    int limit = ChangeQueryBuilder.hasLimit(in)
+        ? ChangeQueryBuilder.getLimit(in)
+        : MAX_LIMIT;
+
+    Predicate<ChangeData> out = rewriteImpl(in, index, limit);
+    if (in == out || out instanceof IndexPredicate) {
+      return new IndexedChangeQuery(db, index, out, limit);
+    } else if (out == null /* cannot rewrite */) {
+      return in;
+    } else {
+      return out;
+    }
+  }
+
+  /**
+   * Rewrite a single predicate subtree.
+   *
+   * @param in predicate to rewrite.
+   * @param index index whose schema determines which fields are indexed.
+   * @param limit maximum number of results to return.
+   * @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
+   *     semantically equivalent, with some of its subtrees wrapped to query the
+   *     index directly.
+   * @throws QueryParseException if the underlying index implementation does not
+   *     support this predicate.
+   */
+  private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in,
+      ChangeIndex index, int limit) throws QueryParseException {
+    if (isIndexPredicate(in, index)) {
+      return in;
+    } else if (!isRewritePossible(in)) {
+      return null; // magic to indicate "in" cannot be rewritten
+    }
+
+    int n = in.getChildCount();
+    BitSet isIndexed = new BitSet(n);
+    BitSet notIndexed = new BitSet(n);
+    BitSet rewritten = new BitSet(n);
+    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);
+      if (nc == c) {
+        isIndexed.set(i);
+        newChildren.add(c);
+      } else if (nc == null /* cannot rewrite c */) {
+        notIndexed.set(i);
+        newChildren.add(c);
+      } else {
+        rewritten.set(i);
+        newChildren.add(nc);
+      }
+    }
+
+    if (isIndexed.cardinality() == n) {
+      return in; // All children are indexed, leave as-is for parent.
+    } else if (notIndexed.cardinality() == n) {
+      return null; // Can't rewrite any children, so cannot rewrite in.
+    } else if (rewritten.cardinality() == n) {
+      return in.copy(newChildren); // All children were rewritten.
+    }
+    return partitionChildren(in, newChildren, isIndexed, index, limit);
+  }
+
+  private boolean isIndexPredicate(Predicate<ChangeData> in, ChangeIndex index) {
+    if (!(in instanceof IndexPredicate)) {
+      return false;
+    }
+    IndexPredicate<ChangeData> p = (IndexPredicate<ChangeData>) in;
+    return index.getSchema().getFields().containsKey(p.getField().getName());
+  }
+
+  private Predicate<ChangeData> partitionChildren(
+      Predicate<ChangeData> in,
+      List<Predicate<ChangeData>> newChildren,
+      BitSet isIndexed,
+      ChangeIndex index,
+      int limit) throws QueryParseException {
+    if (isIndexed.cardinality() == 1) {
+      int i = isIndexed.nextSetBit(0);
+      newChildren.add(
+          0, new IndexedChangeQuery(db, index, newChildren.remove(i), limit));
+      return copy(in, newChildren);
+    }
+
+    // Group all indexed predicates into a wrapped subtree.
+    List<Predicate<ChangeData>> indexed =
+        Lists.newArrayListWithCapacity(isIndexed.cardinality());
+
+    List<Predicate<ChangeData>> all =
+        Lists.newArrayListWithCapacity(
+            newChildren.size() - isIndexed.cardinality() + 1);
+
+    for (int i = 0; i < newChildren.size(); i++) {
+      Predicate<ChangeData> c = newChildren.get(i);
+      if (isIndexed.get(i)) {
+        indexed.add(c);
+      } else {
+        all.add(c);
+      }
+    }
+    all.add(0, new IndexedChangeQuery(db, index, in.copy(indexed), limit));
+    return copy(in, all);
+  }
+
+  private Predicate<ChangeData> copy(
+      Predicate<ChangeData> in,
+      List<Predicate<ChangeData>> all) {
+    if (in instanceof AndPredicate) {
+      return new AndSource(db, all);
+    } else if (in instanceof OrPredicate) {
+      return new OrSource(all);
+    }
+    return in.copy(all);
+  }
+
+  private static boolean isRewritePossible(Predicate<ChangeData> p) {
+    return p.getChildCount() > 0 && (
+           p instanceof AndPredicate
+        || p instanceof OrPredicate
+        || p instanceof NotPredicate);
+  }
+
+  static class BasicRewritesImpl extends BasicChangeRewrites {
+    private static final QueryRewriter.Definition<ChangeData, BasicRewritesImpl> mydef =
+        new QueryRewriter.Definition<ChangeData, BasicRewritesImpl>(
+            BasicRewritesImpl.class, SqlRewriterImpl.BUILDER);
+    @Inject
+    BasicRewritesImpl(Provider<ReviewDb> db, IndexCollection indexes) {
+      super(mydef, db, indexes);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
new file mode 100644
index 0000000..27b2f4b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.Paginated;
+import com.google.gerrit.server.query.change.SortKeyPredicate;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Provider;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Wrapper combining an {@link IndexPredicate} together with a
+ * {@link ChangeDataSource} that returns matching results from the index.
+ * <p>
+ * Appropriate to return as the rootmost predicate that can be processed using
+ * the secondary index; such predicates must also implement
+ * {@link ChangeDataSource} to be chosen by the query processor.
+ */
+public class IndexedChangeQuery extends Predicate<ChangeData>
+    implements ChangeDataSource, Paginated {
+
+  /**
+   * Replace all {@link SortKeyPredicate}s in a tree.
+   * <p>
+   * Strictly speaking this should replace only the {@link SortKeyPredicate} at
+   * the top-level AND node, but this implementation is simpler, and the
+   * behavior of having multiple sortkey operators is undefined anyway.
+   *
+   * @param p predicate to replace in.
+   * @param newValue new cut value to replace all sortkey operators with.
+   * @return a copy of {@code p} with all sortkey predicates replaced; or p
+   *     itself.
+   */
+  @VisibleForTesting
+  static Predicate<ChangeData> replaceSortKeyPredicates(
+      Predicate<ChangeData> p, String newValue) {
+    if (p instanceof SortKeyPredicate) {
+      return ((SortKeyPredicate) p).copy(newValue);
+    } else if (p.getChildCount() > 0) {
+      List<Predicate<ChangeData>> newChildren =
+          Lists.newArrayListWithCapacity(p.getChildCount());
+      boolean replaced = false;
+      for (Predicate<ChangeData> c : p.getChildren()) {
+        Predicate<ChangeData> nc = replaceSortKeyPredicates(c, newValue);
+        newChildren.add(nc);
+        if (nc != c) {
+          replaced = true;
+        }
+      }
+      return replaced ? p.copy(newChildren) : p;
+    } else {
+      return p;
+    }
+  }
+
+  private final Provider<ReviewDb> db;
+  private final ChangeIndex index;
+  private final int limit;
+
+  private Predicate<ChangeData> pred;
+  private ChangeDataSource source;
+
+  public IndexedChangeQuery(Provider<ReviewDb> db, ChangeIndex index,
+      Predicate<ChangeData> pred, int limit) throws QueryParseException {
+    this.db = db;
+    this.index = index;
+    this.limit = limit;
+    this.pred = pred;
+    this.source = index.getSource(pred, limit);
+  }
+
+  @Override
+  public int getChildCount() {
+    return 1;
+  }
+
+  @Override
+  public Predicate<ChangeData> getChild(int i) {
+    if (i == 0) {
+      return pred;
+    }
+    throw new ArrayIndexOutOfBoundsException(i);
+  }
+
+  @Override
+  public List<Predicate<ChangeData>> getChildren() {
+    return ImmutableList.of(pred);
+  }
+
+  @Override
+  public int limit() {
+    return limit;
+  }
+
+  @Override
+  public int getCardinality() {
+    return source != null ? source.getCardinality() : limit();
+  }
+
+  @Override
+  public boolean hasChange() {
+    return index.getSchema().getFields()
+        .containsKey(ChangeField.CHANGE.getName());
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    final ChangeDataSource currSource = source;
+    final ResultSet<ChangeData> rs = currSource.read();
+
+    return new ResultSet<ChangeData>() {
+      @Override
+      public Iterator<ChangeData> iterator() {
+        return Iterables.transform(
+            rs,
+            new Function<ChangeData, ChangeData>() {
+              @Override
+              public
+              ChangeData apply(ChangeData input) {
+                input.cacheFromSource(currSource);
+                return input;
+              }
+            }).iterator();
+      }
+
+      @Override
+      public List<ChangeData> toList() {
+        List<ChangeData> r = rs.toList();
+        for (ChangeData cd : r) {
+          cd.cacheFromSource(currSource);
+        }
+        return r;
+      }
+
+      @Override
+      public void close() {
+        rs.close();
+      }
+    };
+  }
+
+  @Override
+  public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
+    pred = replaceSortKeyPredicates(pred, last.change(db).getSortKey());
+    try {
+      source = index.getSource(pred, limit);
+    } catch (QueryParseException e) {
+      // Don't need to show this exception to the user; the only thing that
+      // changed about pred was its SortKeyPredicates, and any other QPEs
+      // that might happen should have already thrown from the constructor.
+      throw new OrmException(e);
+    }
+    return read();
+  }
+
+  @Override
+  public Predicate<ChangeData> copy(
+      Collection<? extends Predicate<ChangeData>> children) {
+    return this;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return (source != null && cd.isFromSource(source)) || pred.match(cd);
+  }
+
+  @Override
+  public int getCost() {
+    // Index queries are assumed to be cheaper than any other type of query, so
+    // so try to make sure they get picked. Note that pred's cost may be higher
+    // because it doesn't know whether it's being used in an index query or not.
+    return 1;
+  }
+
+  @Override
+  public int hashCode() {
+    return pred.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    IndexedChangeQuery o = (IndexedChangeQuery) other;
+    return pred.equals(o.pred)
+        && limit == o.limit;
+  }
+
+  @Override
+  public String toString() {
+    return Objects.toStringHelper("index")
+        .add("p", pred)
+        .add("limit", limit)
+        .toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
new file mode 100644
index 0000000..8c552d8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
+import com.google.inject.AbstractModule;
+
+public class NoIndexModule extends AbstractModule {
+  // TODO(dborowitz): This module should go away when the index becomes
+  // obligatory, as should the interfaces that exist only to support the
+  // non-index case.
+
+  @Override
+  protected void configure() {
+    bind(ChangeIndex.class).toInstance(ChangeIndex.DISABLED);
+    bind(ChangeIndexer.class).toInstance(ChangeIndexer.DISABLED);
+    bind(ChangeQueryRewriter.class).to(SqlRewriterImpl.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java
new file mode 100644
index 0000000..198c7b0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+public abstract class RegexPredicate<I> extends IndexPredicate<I> {
+  protected RegexPredicate(FieldDef<I, ?> def, String value) {
+    super(def, value);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
new file mode 100644
index 0000000..bda2ace
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicates;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gwtorm.server.OrmException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+
+/** Specific version of a secondary index schema. */
+public class Schema<T> {
+  private static final Logger log = LoggerFactory.getLogger(Schema.class);
+
+  public static class Values<T> {
+    private final FieldDef<T, ?> field;
+    private final Iterable<?> values;
+
+    private Values(FieldDef<T, ?> field, Iterable<?> values) {
+      this.field = field;
+      this.values = values;
+    }
+
+    public FieldDef<T, ?> getField() {
+      return field;
+    }
+
+    public Iterable<?> getValues() {
+      return values;
+    }
+  }
+
+  private final boolean release;
+  private final ImmutableMap<String, FieldDef<T, ?>> fields;
+  private int version;
+
+  protected Schema(boolean release, Iterable<FieldDef<T, ?>> fields) {
+    this(0, release, fields);
+  }
+
+  @VisibleForTesting
+  public Schema(int version, boolean release,
+      Iterable<FieldDef<T, ?>> fields) {
+    this.version = version;
+    this.release = release;
+    ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
+    for (FieldDef<T, ?> f : fields) {
+      b.put(f.getName(), f);
+    }
+    this.fields = b.build();
+  }
+
+  public final boolean isRelease() {
+    return release;
+  }
+
+  public final int getVersion() {
+    return version;
+  }
+
+  public final ImmutableMap<String, FieldDef<T, ?>> getFields() {
+    return fields;
+  }
+
+  /**
+   * Build all fields in the schema from an input object.
+   * <p>
+   * Null values are omitted, as are fields which cause errors, which are
+   * logged.
+   *
+   * @param obj input object.
+   * @param fillArgs arguments for filling fields.
+   * @return all non-null field values from the object.
+   */
+  public final Iterable<Values<T>> buildFields(
+      final T obj, final FillArgs fillArgs) {
+    return FluentIterable.from(fields.values())
+        .transform(new Function<FieldDef<T, ?>, Values<T>>() {
+          @Override
+          public Values<T> apply(FieldDef<T, ?> f) {
+            Object v;
+            try {
+              v = f.get(obj, fillArgs);
+            } catch (OrmException e) {
+              log.error(String.format("error getting field %s of %s",
+                  f.getName(), obj), e);
+              return null;
+            }
+            if (v == null) {
+              return null;
+            } else if (f.isRepeatable()) {
+              return new Values<T>(f, (Iterable<?>) v);
+            } else {
+              return new Values<T>(f, Collections.singleton(v));
+            }
+          }
+        }).filter(Predicates.notNull());
+  }
+
+  @Override
+  public String toString() {
+    return Objects.toStringHelper(this)
+        .addValue(fields.keySet())
+        .toString();
+  }
+
+  void setVersion(int version) {
+    this.version = version;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
new file mode 100644
index 0000000..62fba12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import java.sql.Timestamp;
+
+public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
+  protected TimestampRangePredicate(FieldDef<I, Timestamp> def,
+      String name, String value) {
+    super(def, name, value);
+  }
+
+  public abstract Timestamp getMinTimestamp();
+  public abstract Timestamp getMaxTimestamp();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index b16ab2a..cd6409e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -14,22 +14,29 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.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.server.change.PostReview.NotifyHandling;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -37,6 +44,9 @@
 
 /** Send comments, after the author of them hit used Publish Comments in the UI. */
 public class CommentSender extends ReplyToChangeSender {
+  private static final Logger log = LoggerFactory
+      .getLogger(CommentSender.class);
+
   public static interface Factory {
     public CommentSender create(NotifyHandling notify, Change change);
   }
@@ -62,9 +72,7 @@
         paths.add(p.getFileName());
       }
     }
-    String[] names = paths.toArray(new String[paths.size()]);
-    Arrays.sort(names);
-    changeData.setCurrentFilePaths(names);
+    changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths));
   }
 
   @Override
@@ -99,8 +107,7 @@
   }
 
   public String getInlineComments(int lines) {
-    StringBuilder  cmts = new StringBuilder();
-
+    StringBuilder cmts = new StringBuilder();
     final Repository repo = getRepository();
     try {
       PatchList patchList = null;
@@ -116,55 +123,37 @@
       PatchFile currentFileData = null;
       for (final PatchLineComment c : inlineComments) {
         final Patch.Key pk = c.getKey().getParentKey();
-        final int lineNbr = c.getLine();
-        final short side = c.getSide();
 
         if (!pk.equals(currentFileKey)) {
-          cmts.append("....................................................\n");
+          String link = makeLink(pk);
+          if (link != null) {
+            cmts.append(link).append('\n');
+          }
           if (Patch.COMMIT_MSG.equals(pk.get())) {
-            cmts.append("Commit Message\n");
+            cmts.append("Commit Message:\n\n");
           } else {
-            cmts.append("File ");
-            cmts.append(pk.get());
-            cmts.append("\n");
+            cmts.append("File ").append(pk.get()).append(":\n\n");
           }
           currentFileKey = pk;
 
           if (patchList != null) {
             try {
               currentFileData =
-                  new PatchFile(repo, patchList, pk.getFileName());
+                  new PatchFile(repo, patchList, pk.get());
             } catch (IOException e) {
-              // Don't quote the line if we can't load it.
+              log.warn(String.format(
+                  "Cannot load %s from %s in %s",
+                  pk.getFileName(),
+                  patchList.getNewId().name(),
+                  projectState.getProject().getName()), e);
+              currentFileData = null;
             }
-          } else {
-            currentFileData = null;
           }
         }
 
         if (currentFileData != null) {
-          int maxLines;
-          try {
-            maxLines = currentFileData.getLineCount(side);
-          } catch (Throwable e) {
-            maxLines = lineNbr;
-          }
-
-          final int startLine = Math.max(1, lineNbr - lines + 1);
-          final int stopLine = Math.min(maxLines, lineNbr + lines);
-
-          for (int line = startLine; line <= lineNbr; ++line) {
-            appendFileLine(cmts, currentFileData, side, line);
-          }
-
-          cmts.append(c.getMessage().trim());
-          cmts.append("\n");
-
-          for (int line = lineNbr + 1; line < stopLine; ++line) {
-            appendFileLine(cmts, currentFileData, side, line);
-          }
+          appendComment(cmts, lines, currentFileData, c);
         }
-
         cmts.append("\n\n");
       }
     } finally {
@@ -175,6 +164,59 @@
     return cmts.toString();
   }
 
+  private void appendComment(StringBuilder out, int contextLines,
+      PatchFile currentFileData, PatchLineComment comment) {
+    short side = comment.getSide();
+    CommentRange range = comment.getRange();
+    if (range != null) {
+      String prefix = String.format("Line %d: ", range.getStartLine());
+      for (int n = range.getStartLine(); n <= range.getEndLine(); n++) {
+        out.append(n == range.getStartLine()
+            ? prefix
+            : Strings.padStart(": ", prefix.length(), ' '));
+        try {
+          String s = currentFileData.getLine(side, n);
+          if (n == range.getStartLine() && n == range.getEndLine()) {
+            s = s.substring(
+                Math.min(range.getStartCharacter(), s.length()),
+                Math.min(range.getEndCharacter(), s.length()));
+          } else if (n == range.getStartLine()) {
+            s = s.substring(Math.min(range.getStartCharacter(), s.length()));
+          } else if (n == range.getEndLine()) {
+            s = s.substring(0, Math.min(range.getEndCharacter(), s.length()));
+          }
+          out.append(s);
+        } catch (Throwable e) {
+          // Don't quote the line if we can't safely convert it.
+        }
+        out.append('\n');
+      }
+      appendQuotedParent(out, comment);
+      out.append(comment.getMessage().trim()).append('\n');
+    } else {
+      int lineNbr = comment.getLine();
+      int maxLines;
+      try {
+        maxLines = currentFileData.getLineCount(side);
+      } catch (Throwable e) {
+        maxLines = lineNbr;
+      }
+
+      final int startLine = Math.max(1, lineNbr - contextLines + 1);
+      final int stopLine = Math.min(maxLines, lineNbr + contextLines);
+
+      for (int line = startLine; line <= lineNbr; ++line) {
+        appendFileLine(out, currentFileData, side, line);
+      }
+      appendQuotedParent(out, comment);
+      out.append(comment.getMessage().trim()).append('\n');
+
+      for (int line = lineNbr + 1; line < stopLine; ++line) {
+        appendFileLine(out, currentFileData, side, line);
+      }
+    }
+  }
+
   private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) {
     cmts.append("Line " + line);
     try {
@@ -187,6 +229,48 @@
     cmts.append("\n");
   }
 
+  private void appendQuotedParent(StringBuilder out, PatchLineComment child) {
+    if (child.getParentUuid() != null) {
+      PatchLineComment parent;
+      try {
+        parent = args.db.get().patchComments().get(
+            new PatchLineComment.Key(
+                child.getKey().getParentKey(),
+                child.getParentUuid()));
+      } catch (OrmException e) {
+        parent = null;
+      }
+      if (parent != null) {
+        String msg = parent.getMessage().trim();
+        if (msg.length() > 75) {
+          msg = msg.substring(0, 75);
+        }
+        int lf = msg.indexOf('\n');
+        if (lf > 0) {
+          msg = msg.substring(0, lf);
+        }
+        out.append("> ").append(msg).append('\n');
+      }
+    }
+  }
+
+  // Makes a link back to the given patch set and file.
+  private String makeLink(Patch.Key patch) {
+    String url = getGerritUrl();
+    if (url == null) {
+      return null;
+    }
+
+    PatchSet.Id ps = patch.getParentKey();
+    Change.Id c = ps.getParentKey();
+    return new StringBuilder()
+      .append(url)
+      .append("#/c/").append(c)
+      .append('/').append(ps.get())
+      .append('/').append(KeyUtil.encode(patch.get()))
+      .toString();
+  }
+
   private Repository getRepository() {
     try {
       return args.server.openRepository(projectState.getProject().getNameKey());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 3e50283..d7dfd3d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -30,7 +31,6 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -39,8 +39,6 @@
 
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 class EmailArguments {
   final GitRepositoryManager server;
   final ProjectCache projectCache;
@@ -60,7 +58,6 @@
   final List<String> sshAddresses;
 
   final ChangeQueryBuilder.Factory queryBuilder;
-  final Provider<ChangeQueryRewriter> queryRewriter;
   final Provider<ReviewDb> db;
   final RuntimeInstance velocityRuntime;
   final EmailSettings settings;
@@ -78,7 +75,7 @@
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder.Factory queryBuilder,
-      Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
+      Provider<ReviewDb> db,
       RuntimeInstance velocityRuntime,
       EmailSettings settings,
       @SshAdvertisedAddresses List<String> sshAddresses) {
@@ -98,7 +95,6 @@
     this.urlProvider = urlProvider;
     this.allProjectsName = allProjectsName;
     this.queryBuilder = queryBuilder;
-    this.queryRewriter = queryRewriter;
     this.db = db;
     this.velocityRuntime = velocityRuntime;
     this.settings = settings;
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 ce50002..dc09a9c 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
@@ -87,6 +87,7 @@
 
     init();
     format();
+    appendText(velocifyFile("Footer.vm"));
     if (shouldSendMessage()) {
       if (fromId != null) {
         final Account fromUser = args.accountCache.get(fromId).getAccount();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
new file mode 100644
index 0000000..3484dd8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
@@ -0,0 +1,149 @@
+// 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.getRecipientsFromApprovals;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
+import com.google.gerrit.common.ChangeHooks;
+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.PatchSetApproval;
+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.index.ChangeIndexer;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+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 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(ReviewDb db,
+      ChangeHooks hooks,
+      GitRepositoryManager repoManager,
+      PatchSetInfoFactory patchSetInfoFactory,
+      ApprovalsUtil approvalsUtil,
+      AccountResolver accountResolver,
+      CreateChangeSender.Factory createChangeSenderFactory,
+      ReplacePatchSetSender.Factory replacePatchSetFactory,
+      ChangeIndexer indexer) {
+    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 boolean newChange,
+      final IdentifiedUser currentUser, final Change updatedChange,
+      final PatchSet updatedPatchSet, final LabelTypes labelTypes)
+      throws OrmException, IOException, PatchSetInfoNotAvailableException {
+    final Repository git = repoManager.openRepository(updatedChange.getProject());
+    try {
+      final RevWalk revWalk = new RevWalk(git);
+      final RevCommit commit;
+      try {
+        commit = revWalk.parseCommit(ObjectId.fromString(
+            updatedPatchSet.getRevision().get()));
+      } finally {
+        revWalk.release();
+      }
+      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, labelTypes,
+            updatedChange, updatedPatchSet, info,
+            recipients.getReviewers(), Collections.<Account.Id> emptySet());
+        try {
+          CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
+          cm.setFrom(me);
+          cm.setPatchSet(updatedPatchSet, info);
+          cm.addReviewers(recipients.getReviewers());
+          cm.addExtraCC(recipients.getCcOnly());
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new change " + updatedChange.getId(), e);
+        }
+      } else {
+        final List<PatchSetApproval> patchSetApprovals =
+            db.patchSetApprovals().byChange(
+                updatedChange.getId()).toList();
+        final MailRecipients oldRecipients =
+            getRecipientsFromApprovals(patchSetApprovals);
+        approvalsUtil.addReviewers(db, labelTypes, updatedChange,
+            updatedPatchSet, info, recipients.getReviewers(),
+            oldRecipients.getAll());
+        final ChangeMessage msg =
+            new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
+                ChangeUtil.messageUUID(db)), me,
+                updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
+        msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
+        try {
+          ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
+          cm.setFrom(me);
+          cm.setPatchSet(updatedPatchSet, info);
+          cm.setChangeMessage(msg);
+          cm.addReviewers(recipients.getReviewers());
+          cm.addExtraCC(recipients.getCcOnly());
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
+        }
+      }
+    } finally {
+      git.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 84304b8..757562c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -24,8 +24,8 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+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;
@@ -88,9 +88,9 @@
             add(matching, nc, state.getProject().getNameKey());
           } catch (QueryParseException e) {
             log.warn(String.format(
-                "Project %s has invalid notify %s filter \"%s\": %s",
+                "Project %s has invalid notify %s filter \"%s\"",
                 state.getProject().getName(), nc.getName(),
-                nc.getFilter(), e.getMessage()));
+                nc.getFilter()), e);
           }
         }
       }
@@ -202,14 +202,13 @@
     }
 
     if (filter != null) {
-      qb.setAllowFile(true);
+      qb.setAllowFileRegex(true);
       Predicate<ChangeData> filterPredicate = qb.parse(filter);
       if (p == null) {
         p = filterPredicate;
       } else {
         p = Predicate.and(filterPredicate, p);
       }
-      p = args.queryRewriter.get().rewrite(p);
     }
     return p == null ? true : p.match(changeData);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
deleted file mode 100644
index f9a85b9..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
+++ /dev/null
@@ -1,37 +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.mail;
-
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-/** Send notice to reviewers that a change has been rebased. */
-public class RebasedPatchSetSender extends ReplacePatchSetSender {
-  public static interface Factory {
-    RebasedPatchSetSender create(Change change);
-  }
-
-  @Inject
-  public RebasedPatchSetSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c);
-  }
-
-  @Override
-  protected void formatChange() throws EmailException {
-    appendText(velocifyFile("RebasedPatchSet.vm"));
-  }
-}
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 72df560..89fc6b9 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
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -151,9 +152,9 @@
       setMissingHeader(hdrs, "Importance", importance);
     }
     if(expiryDays > 0) {
-      Date expiry = new Date(System.currentTimeMillis() + 
-        expiryDays * 24 * 60 * 60 * 1000 );
-      setMissingHeader(hdrs, "Expiry-Date", 
+      Date expiry = new Date(TimeUtil.nowMs() +
+        expiryDays * 24 * 60 * 60 * 1000L );
+      setMissingHeader(hdrs, "Expiry-Date",
         new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry));
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index b95994f..52cba09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -248,12 +248,14 @@
           //
           if (ab < ae //
               && (ab == 0 || a.charAt(ab - 1) == '\n') //
-              && ae < a.size() && a.charAt(ae) == '\n') {
+              && ae < a.size() && a.charAt(ae - 1) != '\n'
+              && a.charAt(ae) == '\n') {
             ae++;
           }
           if (bb < be //
               && (bb == 0 || b.charAt(bb - 1) == '\n') //
-              && be < b.size() && b.charAt(be) == '\n') {
+              && be < b.size() && b.charAt(be - 1) != '\n'
+              && b.charAt(be) == '\n') {
             be++;
           }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 6fcf581..81f4352 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -31,7 +31,6 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 
 import java.io.IOException;
-import java.nio.charset.CharacterCodingException;
 
 /** State supporting processing of a single {@link Patch} instance. */
 public class PatchFile {
@@ -89,7 +88,6 @@
    * @throws CorruptEntityException the patch cannot be read.
    * @throws IOException the patch or complete file content cannot be read.
    * @throws NoSuchEntityException
-   * @throws CharacterCodingException the file is not a known character set.
    */
   public String getLine(final int file, final int line)
       throws CorruptEntityException, IOException, NoSuchEntityException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index 93d7bf7..59e3050 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -24,6 +24,7 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 
@@ -44,8 +45,6 @@
 import java.util.zip.DeflaterOutputStream;
 import java.util.zip.InflaterInputStream;
 
-import javax.annotation.Nullable;
-
 public class PatchList implements Serializable {
   private static final long serialVersionUID = PatchListKey.serialVersionUID;
   private static final Comparator<PatchListEntry> PATCH_CMP =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 967e6a7..7b7c731 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -90,6 +90,10 @@
       throws PatchListNotAvailableException {
     final Project.NameKey projectKey = change.getProject();
     final ObjectId a = null;
+    if (patchSet.getRevision() == null) {
+      throw new PatchListNotAvailableException(
+          "revision is null for " + patchSet.getId());
+    }
     final ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
     final Whitespace ws = Whitespace.IGNORE_NONE;
     return get(new PatchListKey(projectKey, a, b, ws));
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 ff9e6cf..852165c 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
@@ -171,7 +171,10 @@
     final List<String> headerLines = new ArrayList<String>(m.size() - 1);
     for (int i = 1; i < m.size() - 1; i++) {
       final int b = m.get(i);
-      final int e = m.get(i + 1);
+      int e = m.get(i + 1);
+      if (header[e - 1] == '\n') {
+        e--;
+      }
       headerLines.add(RawParseUtils.decode(Constants.CHARSET, header, b, e));
     }
     return headerLines;
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 de12cc6..c36fbc0 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
@@ -21,6 +21,7 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 
@@ -32,8 +33,6 @@
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
 
-import javax.annotation.Nullable;
-
 public class PatchListKey implements Serializable {
   static final long serialVersionUID = 16L;
 
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 bb231a5..e1294e0 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
@@ -64,7 +64,7 @@
 import java.util.List;
 import java.util.Map;
 
-class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
+public class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
   static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
 
   private final GitRepositoryManager repoManager;
@@ -241,8 +241,13 @@
     }
   }
 
-  private static RevObject automerge(Repository repo, RevWalk rw, RevCommit b)
+  public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b)
       throws IOException {
+    return automerge(repo, rw, b, true);
+  }
+
+  public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b,
+      boolean save) throws IOException {
     String hash = b.name();
     String refName = GitRepositoryManager.REFS_CACHE_AUTOMERGE
         + hash.substring(0, 2)
@@ -373,10 +378,12 @@
       ins.release();
     }
 
-    RefUpdate update = repo.updateRef(refName);
-    update.setNewObjectId(treeId);
-    update.disableRefLog();
-    update.forceUpdate();
+    if (save) {
+      RefUpdate update = repo.updateRef(refName);
+      update.setNewObjectId(treeId);
+      update.disableRefLog();
+      update.forceUpdate();
+    }
     return rw.parseTree(treeId);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
new file mode 100644
index 0000000..0db7dee
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -0,0 +1,517 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.gerrit.common.data.CommentDetail;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.prettify.common.EditList;
+import com.google.gerrit.prettify.common.SparseFileContent;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.server.FileTypeRegistry;
+import com.google.inject.Inject;
+
+import eu.medsea.mimeutil.MimeType;
+import eu.medsea.mimeutil.MimeUtil2;
+
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+class PatchScriptBuilder {
+  static final int MAX_CONTEXT = 5000000;
+  static final int BIG_FILE = 9000;
+
+  private static final Comparator<Edit> EDIT_SORT = new Comparator<Edit>() {
+    @Override
+    public int compare(final Edit o1, final Edit o2) {
+      return o1.getBeginA() - o2.getBeginA();
+    }
+  };
+
+  private Repository db;
+  private Project.NameKey projectKey;
+  private ObjectReader reader;
+  private Change change;
+  private AccountDiffPreference diffPrefs;
+  private boolean againstParent;
+  private ObjectId aId;
+  private ObjectId bId;
+
+  private final Side a;
+  private final Side b;
+
+  private List<Edit> edits;
+  private final FileTypeRegistry registry;
+  private final PatchListCache patchListCache;
+  private int context;
+
+  @Inject
+  PatchScriptBuilder(final FileTypeRegistry ftr, final PatchListCache plc) {
+    a = new Side();
+    b = new Side();
+    registry = ftr;
+    patchListCache = plc;
+  }
+
+  void setRepository(Repository r, Project.NameKey projectKey) {
+    this.db = r;
+    this.projectKey = projectKey;
+  }
+
+  void setChange(final Change c) {
+    this.change = c;
+  }
+
+  void setDiffPrefs(final AccountDiffPreference dp) {
+    diffPrefs = dp;
+
+    context = diffPrefs.getContext();
+    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+      context = MAX_CONTEXT;
+    } else if (context > MAX_CONTEXT) {
+      context = MAX_CONTEXT;
+    }
+  }
+
+  void setTrees(final boolean ap, final ObjectId a, final ObjectId b) {
+    againstParent = ap;
+    aId = a;
+    bId = b;
+  }
+
+  PatchScript toPatchScript(final PatchListEntry content,
+      final CommentDetail comments, final List<Patch> history)
+      throws IOException {
+    reader = db.newObjectReader();
+    try {
+      return build(content, comments, history);
+    } finally {
+      reader.release();
+    }
+  }
+
+  private PatchScript build(final PatchListEntry content,
+      final CommentDetail comments, final List<Patch> history)
+      throws IOException {
+    boolean intralineDifferenceIsPossible = true;
+    boolean intralineFailure = false;
+    boolean intralineTimeout = false;
+
+    a.path = oldName(content);
+    b.path = newName(content);
+
+    a.resolve(null, aId);
+    b.resolve(a, bId);
+
+    edits = new ArrayList<Edit>(content.getEdits());
+
+    if (!isModify(content)) {
+      intralineDifferenceIsPossible = false;
+    } else if (diffPrefs.isIntralineDifference()) {
+      IntraLineDiff d =
+          patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
+              b.id, b.src, edits, projectKey, bId, b.path,
+              diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE));
+      if (d != null) {
+        switch (d.getStatus()) {
+          case EDIT_LIST:
+            edits = new ArrayList<Edit>(d.getEdits());
+            break;
+
+          case DISABLED:
+            intralineDifferenceIsPossible = false;
+            break;
+
+          case ERROR:
+            intralineDifferenceIsPossible = false;
+            intralineFailure = true;
+            break;
+
+          case TIMEOUT:
+            intralineDifferenceIsPossible = false;
+            intralineTimeout = true;
+            break;
+        }
+      } else {
+        intralineDifferenceIsPossible = false;
+        intralineFailure = true;
+      }
+    }
+
+    ensureCommentsVisible(comments);
+
+    boolean hugeFile = false;
+    if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
+
+    } else if (a.src == b.src && a.size() <= context
+        && content.getEdits().isEmpty()) {
+      // Odd special case; the files are identical (100% rename or copy)
+      // and the user has asked for context that is larger than the file.
+      // Send them the entire file, with an empty edit after the last line.
+      //
+      for (int i = 0; i < a.size(); i++) {
+        a.addLine(i);
+      }
+      edits = new ArrayList<Edit>(1);
+      edits.add(new Edit(a.size(), a.size()));
+
+    } else {
+      if (BIG_FILE < Math.max(a.size(), b.size())) {
+        // IF the file is really large, we disable things to avoid choking
+        // the browser client.
+        //
+        hugeFile = true;
+
+      }
+
+      // In order to expand the skipped common lines or syntax highlight the
+      // file properly we need to give the client the complete file contents.
+      // So force our context temporarily to the complete file size.
+      //
+      context = MAX_CONTEXT;
+
+      packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
+    }
+
+    return new PatchScript(change.getKey(), content.getChangeType(),
+        content.getOldName(), content.getNewName(), a.fileMode, b.fileMode,
+        content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
+        a.displayMethod, b.displayMethod, a.mimeType.toString(),
+        b.mimeType.toString(), comments, history, hugeFile,
+        intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
+  }
+
+  private static boolean isModify(PatchListEntry content) {
+    switch (content.getChangeType()) {
+      case MODIFIED:
+      case COPIED:
+      case RENAMED:
+        return true;
+
+      case ADDED:
+      case DELETED:
+      default:
+        return false;
+    }
+  }
+
+  private static String oldName(final PatchListEntry entry) {
+    switch (entry.getChangeType()) {
+      case ADDED:
+        return null;
+      case DELETED:
+      case MODIFIED:
+        return entry.getNewName();
+      case COPIED:
+      case RENAMED:
+      default:
+        return entry.getOldName();
+    }
+  }
+
+  private static String newName(final PatchListEntry entry) {
+    switch (entry.getChangeType()) {
+      case DELETED:
+        return null;
+      case ADDED:
+      case MODIFIED:
+      case COPIED:
+      case RENAMED:
+      default:
+        return entry.getNewName();
+    }
+  }
+
+  private void ensureCommentsVisible(final CommentDetail comments) {
+    if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) {
+      // No comments, no additional dummy edits are required.
+      //
+      return;
+    }
+
+    // Construct empty Edit blocks around each location where a comment is.
+    // This will force the later packContent method to include the regions
+    // containing comments, potentially combining those regions together if
+    // they have overlapping contexts. UI renders will also be able to make
+    // correct hunks from this, but because the Edit is empty they will not
+    // style it specially.
+    //
+    final List<Edit> empty = new ArrayList<Edit>();
+    int lastLine;
+
+    lastLine = -1;
+    for (PatchLineComment plc : comments.getCommentsA()) {
+      final int a = plc.getLine();
+      if (lastLine != a) {
+        final int b = mapA2B(a - 1);
+        if (0 <= b) {
+          safeAdd(empty, new Edit(a - 1, b));
+        }
+        lastLine = a;
+      }
+    }
+
+    lastLine = -1;
+    for (PatchLineComment plc : comments.getCommentsB()) {
+      final int b = plc.getLine();
+      if (lastLine != b) {
+        final int a = mapB2A(b - 1);
+        if (0 <= a) {
+          safeAdd(empty, new Edit(a, b - 1));
+        }
+        lastLine = b;
+      }
+    }
+
+    // Sort the final list by the index in A, so packContent can combine
+    // them correctly later.
+    //
+    edits.addAll(empty);
+    Collections.sort(edits, EDIT_SORT);
+  }
+
+  private void safeAdd(final List<Edit> empty, final Edit toAdd) {
+    final int a = toAdd.getBeginA();
+    final int b = toAdd.getBeginB();
+    for (final Edit e : edits) {
+      if (e.getBeginA() <= a && a <= e.getEndA()) {
+        return;
+      }
+      if (e.getBeginB() <= b && b <= e.getEndB()) {
+        return;
+      }
+    }
+    empty.add(toAdd);
+  }
+
+  private int mapA2B(final int a) {
+    if (edits.isEmpty()) {
+      // Magic special case of an unmodified file.
+      //
+      return a;
+    }
+
+    for (int i = 0; i < edits.size(); i++) {
+      final Edit e = edits.get(i);
+      if (a < e.getBeginA()) {
+        if (i == 0) {
+          // Special case of context at start of file.
+          //
+          return a;
+        }
+        return e.getBeginB() - (e.getBeginA() - a);
+      }
+      if (e.getBeginA() <= a && a <= e.getEndA()) {
+        return -1;
+      }
+    }
+
+    final Edit last = edits.get(edits.size() - 1);
+    return last.getEndB() + (a - last.getEndA());
+  }
+
+  private int mapB2A(final int b) {
+    if (edits.isEmpty()) {
+      // Magic special case of an unmodified file.
+      //
+      return b;
+    }
+
+    for (int i = 0; i < edits.size(); i++) {
+      final Edit e = edits.get(i);
+      if (b < e.getBeginB()) {
+        if (i == 0) {
+          // Special case of context at start of file.
+          //
+          return b;
+        }
+        return e.getBeginA() - (e.getBeginB() - b);
+      }
+      if (e.getBeginB() <= b && b <= e.getEndB()) {
+        return -1;
+      }
+    }
+
+    final Edit last = edits.get(edits.size() - 1);
+    return last.getEndA() + (b - last.getEndB());
+  }
+
+  private void packContent(boolean ignoredWhitespace) {
+    EditList list = new EditList(edits, context, a.size(), b.size());
+    for (final EditList.Hunk hunk : list.getHunks()) {
+      while (hunk.next()) {
+        if (hunk.isContextLine()) {
+          final String lineA = a.src.getString(hunk.getCurA());
+          a.dst.addLine(hunk.getCurA(), lineA);
+
+          if (ignoredWhitespace) {
+            // If we ignored whitespace in some form, also get the line
+            // from b when it does not exactly match the line from a.
+            //
+            final String lineB = b.src.getString(hunk.getCurB());
+            if (!lineA.equals(lineB)) {
+              b.dst.addLine(hunk.getCurB(), lineB);
+            }
+          }
+          hunk.incBoth();
+          continue;
+        }
+
+        if (hunk.isDeletedA()) {
+          a.addLine(hunk.getCurA());
+          hunk.incA();
+        }
+
+        if (hunk.isInsertedB()) {
+          b.addLine(hunk.getCurB());
+          hunk.incB();
+        }
+      }
+    }
+  }
+
+  private class Side {
+    String path;
+    ObjectId id;
+    FileMode mode;
+    byte[] srcContent;
+    Text src;
+    MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
+    DisplayMethod displayMethod = DisplayMethod.DIFF;
+    PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
+    final SparseFileContent dst = new SparseFileContent();
+
+    int size() {
+      return src != null ? src.size() : 0;
+    }
+
+    void addLine(int line) {
+      dst.addLine(line, src.getString(line));
+    }
+
+    void resolve(final Side other, final ObjectId within) throws IOException {
+      try {
+        final boolean reuse;
+        if (Patch.COMMIT_MSG.equals(path)) {
+          if (againstParent && (aId == within || within.equals(aId))) {
+            id = ObjectId.zeroId();
+            src = Text.EMPTY;
+            srcContent = Text.NO_BYTES;
+            mode = FileMode.MISSING;
+            displayMethod = DisplayMethod.NONE;
+          } else {
+            id = within;
+            src = Text.forCommit(db, reader, within);
+            srcContent = src.getContent();
+            if (src == Text.EMPTY) {
+              mode = FileMode.MISSING;
+              displayMethod = DisplayMethod.NONE;
+            } else {
+              mode = FileMode.REGULAR_FILE;
+            }
+          }
+          reuse = false;
+
+        } else {
+          final TreeWalk tw = find(within);
+
+          id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
+          mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
+          reuse = other != null && other.id.equals(id) && other.mode == mode;
+
+          if (reuse) {
+            srcContent = other.srcContent;
+
+          } else if (mode.getObjectType() == Constants.OBJ_BLOB) {
+            srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB));
+
+          } else {
+            srcContent = Text.NO_BYTES;
+          }
+
+          if (reuse) {
+            mimeType = other.mimeType;
+            displayMethod = other.displayMethod;
+            src = other.src;
+
+          } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) {
+            mimeType = registry.getMimeType(path, srcContent);
+            if ("image".equals(mimeType.getMediaType())
+                && registry.isSafeInline(mimeType)) {
+              displayMethod = DisplayMethod.IMG;
+            }
+          }
+        }
+
+        if (mode == FileMode.MISSING) {
+          displayMethod = DisplayMethod.NONE;
+        }
+
+        if (!reuse) {
+          if (srcContent == Text.NO_BYTES) {
+            src = Text.EMPTY;
+          } else {
+            src = new Text(srcContent);
+          }
+        }
+
+        if (srcContent.length > 0 && srcContent[srcContent.length - 1] != '\n') {
+          dst.setMissingNewlineAtEnd(true);
+        }
+        dst.setSize(size());
+        dst.setPath(path);
+
+        if (mode == FileMode.SYMLINK) {
+          fileMode = PatchScript.FileMode.SYMLINK;
+        } else if (mode == FileMode.GITLINK) {
+          fileMode = PatchScript.FileMode.GITLINK;
+        }
+      } catch (IOException err) {
+        throw new IOException("Cannot read " + within.name() + ":" + path, err);
+      }
+    }
+
+    private TreeWalk find(final ObjectId within) throws MissingObjectException,
+        IncorrectObjectTypeException, CorruptObjectException, IOException {
+      if (path == null || within == null) {
+        return null;
+      }
+      final RevWalk rw = new RevWalk(reader);
+      final RevTree tree = rw.parseTree(within);
+      return TreeWalk.forPath(reader, path, tree);
+    }
+  }
+}
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
new file mode 100644
index 0000000..ed4470a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -0,0 +1,334 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.CommentDetail;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+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.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+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.AccountInfoCacheFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LargeObjectException;
+import com.google.gerrit.server.project.ChangeControl;
+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.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+
+public class PatchScriptFactory implements Callable<PatchScript> {
+  public interface Factory {
+    PatchScriptFactory create(
+        ChangeControl control,
+        String fileName,
+        @Assisted("patchSetA") PatchSet.Id patchSetA,
+        @Assisted("patchSetB") PatchSet.Id patchSetB,
+        AccountDiffPreference diffPrefs);
+  }
+
+  private static final Logger log =
+      LoggerFactory.getLogger(PatchScriptFactory.class);
+
+  private final GitRepositoryManager repoManager;
+  private final Provider<PatchScriptBuilder> builderFactory;
+  private final PatchListCache patchListCache;
+  private final ReviewDb db;
+  private final AccountInfoCacheFactory.Factory aicFactory;
+
+  private final String fileName;
+  @Nullable
+  private final PatchSet.Id psa;
+  private final PatchSet.Id psb;
+  private final AccountDiffPreference diffPrefs;
+
+  private final Change.Id changeId;
+
+  private Change change;
+  private Project.NameKey projectKey;
+  private ChangeControl control;
+  private ObjectId aId;
+  private ObjectId bId;
+  private List<Patch> history;
+  private CommentDetail comments;
+
+  @Inject
+  PatchScriptFactory(final GitRepositoryManager grm,
+      Provider<PatchScriptBuilder> builderFactory,
+      final PatchListCache patchListCache, final ReviewDb db,
+      final AccountInfoCacheFactory.Factory aicFactory,
+      @Assisted ChangeControl control,
+      @Assisted final String fileName,
+      @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
+      @Assisted("patchSetB") final PatchSet.Id patchSetB,
+      @Assisted final AccountDiffPreference diffPrefs) {
+    this.repoManager = grm;
+    this.builderFactory = builderFactory;
+    this.patchListCache = patchListCache;
+    this.db = db;
+    this.control = control;
+    this.aicFactory = aicFactory;
+
+    this.fileName = fileName;
+    this.psa = patchSetA;
+    this.psb = patchSetB;
+    this.diffPrefs = diffPrefs;
+
+    changeId = patchSetB.getParentKey();
+  }
+
+  @Override
+  public PatchScript call() throws OrmException, NoSuchChangeException,
+      LargeObjectException {
+    validatePatchSetId(psa);
+    validatePatchSetId(psb);
+
+    change = control.getChange();
+    projectKey = change.getProject();
+
+    aId = psa != null ? toObjectId(db, psa) : null;
+    bId = toObjectId(db, psb);
+
+    if ((psa != null && !control.isPatchVisible(db.patchSets().get(psa), db)) ||
+        (psb != null && !control.isPatchVisible(db.patchSets().get(psb), db))) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    final Repository git;
+    try {
+      git = repoManager.openRepository(projectKey);
+    } catch (RepositoryNotFoundException e) {
+      log.error("Repository " + projectKey + " not found", e);
+      throw new NoSuchChangeException(changeId, e);
+    } catch (IOException e) {
+      log.error("Cannot open repository " + projectKey, e);
+      throw new NoSuchChangeException(changeId, e);
+    }
+    try {
+      final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+      final PatchScriptBuilder b = newBuilder(list, git);
+      final PatchListEntry content = list.get(fileName);
+
+      loadCommentsAndHistory(content.getChangeType(), //
+          content.getOldName(), //
+          content.getNewName());
+
+        return b.toPatchScript(content, comments, history);
+    } catch (PatchListNotAvailableException e) {
+      throw new NoSuchChangeException(changeId, e);
+    } catch (IOException e) {
+      log.error("File content unavailable", e);
+      throw new NoSuchChangeException(changeId, e);
+    } catch (org.eclipse.jgit.errors.LargeObjectException err) {
+      throw new LargeObjectException("File content is too large", err);
+    } finally {
+      git.close();
+    }
+  }
+
+  private PatchListKey keyFor(final Whitespace whitespace) {
+    return new PatchListKey(projectKey, aId, bId, whitespace);
+  }
+
+  private PatchList listFor(final PatchListKey key)
+      throws PatchListNotAvailableException {
+    return patchListCache.get(key);
+  }
+
+  private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
+    final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
+    final PatchScriptBuilder b = builderFactory.get();
+    b.setRepository(git, projectKey);
+    b.setChange(change);
+    b.setDiffPrefs(dp);
+    b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
+    return b;
+  }
+
+  private ObjectId toObjectId(final ReviewDb db, final PatchSet.Id psId)
+      throws OrmException, NoSuchChangeException {
+    if (!changeId.equals(psId.getParentKey())) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    final PatchSet ps = db.patchSets().get(psId);
+    if (ps == null || ps.getRevision() == null
+        || ps.getRevision().get() == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    try {
+      return ObjectId.fromString(ps.getRevision().get());
+    } catch (IllegalArgumentException e) {
+      log.error("Patch set " + psId + " has invalid revision");
+      throw new NoSuchChangeException(changeId, e);
+    }
+  }
+
+  private void validatePatchSetId(final PatchSet.Id psId)
+      throws NoSuchChangeException {
+    if (psId == null) { // OK, means use base;
+    } else if (changeId.equals(psId.getParentKey())) { // OK, same change;
+    } else {
+      throw new NoSuchChangeException(changeId);
+    }
+  }
+
+  private void loadCommentsAndHistory(final ChangeType changeType,
+      final String oldName, final String newName) throws OrmException {
+    history = new ArrayList<Patch>();
+    comments = new CommentDetail(psa, psb);
+
+    final Map<Patch.Key, Patch> byKey = new HashMap<Patch.Key, Patch>();
+    final AccountInfoCacheFactory aic = aicFactory.create();
+
+    // This seems like a cheap trick. It doesn't properly account for a
+    // file that gets renamed between patch set 1 and patch set 2. We
+    // will wind up packing the wrong Patch object because we didn't do
+    // proper rename detection between the patch sets.
+    //
+    for (final PatchSet ps : db.patchSets().byChange(changeId)) {
+      if (!control.isPatchVisible(ps, db)) {
+        continue;
+      }
+      String name = fileName;
+      if (psa != null) {
+        switch (changeType) {
+          case COPIED:
+          case RENAMED:
+            if (ps.getId().equals(psa)) {
+              name = oldName;
+            }
+            break;
+
+          case MODIFIED:
+          case DELETED:
+          case ADDED:
+          case REWRITE:
+            break;
+        }
+      }
+
+      final Patch p = new Patch(new Patch.Key(ps.getId(), name));
+      history.add(p);
+      byKey.put(p.getKey(), p);
+    }
+
+    switch (changeType) {
+      case ADDED:
+      case MODIFIED:
+        loadPublished(byKey, aic, newName);
+        break;
+
+      case DELETED:
+        loadPublished(byKey, aic, newName);
+        break;
+
+      case COPIED:
+      case RENAMED:
+        if (psa != null) {
+          loadPublished(byKey, aic, oldName);
+        }
+        loadPublished(byKey, aic, newName);
+        break;
+
+      case REWRITE:
+        break;
+    }
+
+    final CurrentUser user = control.getCurrentUser();
+    if (user.isIdentifiedUser()) {
+      final Account.Id me = ((IdentifiedUser) user).getAccountId();
+      switch (changeType) {
+        case ADDED:
+        case MODIFIED:
+          loadDrafts(byKey, aic, me, newName);
+          break;
+
+        case DELETED:
+          loadDrafts(byKey, aic, me, newName);
+          break;
+
+        case COPIED:
+        case RENAMED:
+          if (psa != null) {
+            loadDrafts(byKey, aic, me, oldName);
+          }
+          loadDrafts(byKey, aic, me, newName);
+          break;
+
+        case REWRITE:
+          break;
+      }
+    }
+
+    comments.setAccountInfoCache(aic.create());
+  }
+
+  private void loadPublished(final Map<Patch.Key, Patch> byKey,
+      final AccountInfoCacheFactory aic, final String file) throws OrmException {
+    for (PatchLineComment c : db.patchComments().publishedByChangeFile(changeId, file)) {
+      if (comments.include(c)) {
+        aic.want(c.getAuthor());
+      }
+
+      final Patch.Key pKey = c.getKey().getParentKey();
+      final Patch p = byKey.get(pKey);
+      if (p != null) {
+        p.setCommentCount(p.getCommentCount() + 1);
+      }
+    }
+  }
+
+  private void loadDrafts(final Map<Patch.Key, Patch> byKey,
+      final AccountInfoCacheFactory aic, final Account.Id me, final String file)
+      throws OrmException {
+    for (PatchLineComment c : db.patchComments().draftByChangeFileAuthor(changeId, file, me)) {
+      if (comments.include(c)) {
+        aic.want(me);
+      }
+
+      final Patch.Key pKey = c.getKey().getParentKey();
+      final Patch p = byKey.get(pKey);
+      if (p != null) {
+        p.setDraftCount(p.getDraftCount() + 1);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index b04f337..593f2c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -32,6 +32,7 @@
     try {
       jarFile.close();
     } catch (IOException err) {
+      PluginLoader.log.error("Cannot close " + jarFile.getName(), err);
     }
     if (!tmpFile.delete() && tmpFile.exists()) {
       PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
index e2cac6e..6684bfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
-import com.google.gerrit.extensions.restapi.PutInput;
+import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -38,7 +38,7 @@
   static class Input {
     @DefaultInput
     String url;
-    PutInput raw;
+    RawInput raw;
   }
 
   private final PluginLoader loader;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java
new file mode 100644
index 0000000..6adc677
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java
@@ -0,0 +1,266 @@
+// 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.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
+
+import java.io.File;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+class JarPlugin extends Plugin {
+
+  /** Unique key that changes whenever a plugin reloads. */
+  public static final class CacheKey {
+    private final String name;
+
+    CacheKey(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      int id = System.identityHashCode(this);
+      return String.format("Plugin[%s@%x]", name, id);
+    }
+  }
+
+  private final JarFile jarFile;
+  private final Manifest manifest;
+  private final File dataDir;
+  private final ClassLoader classLoader;
+  private Class<? extends Module> sysModule;
+  private Class<? extends Module> sshModule;
+  private Class<? extends Module> httpModule;
+
+  private Injector sysInjector;
+  private Injector sshInjector;
+  private Injector httpInjector;
+  private LifecycleManager manager;
+  private List<ReloadableRegistrationHandle<?>> reloadableHandles;
+
+  public JarPlugin(String name,
+      PluginUser pluginUser,
+      File srcJar,
+      FileSnapshot snapshot,
+      JarFile jarFile,
+      Manifest manifest,
+      File dataDir,
+      ApiType apiType,
+      ClassLoader classLoader,
+      @Nullable Class<? extends Module> sysModule,
+      @Nullable Class<? extends Module> sshModule,
+      @Nullable Class<? extends Module> httpModule) {
+    super(name, srcJar, pluginUser, snapshot, apiType);
+    this.jarFile = jarFile;
+    this.manifest = manifest;
+    this.dataDir = dataDir;
+    this.classLoader = classLoader;
+    this.sysModule = sysModule;
+    this.sshModule = sshModule;
+    this.httpModule = httpModule;
+  }
+
+  File getSrcJar() {
+    return getSrcFile();
+  }
+
+  @Nullable
+  public String getVersion() {
+    Attributes main = manifest.getMainAttributes();
+    return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+  }
+
+  boolean canReload() {
+    Attributes main = manifest.getMainAttributes();
+    String v = main.getValue("Gerrit-ReloadMode");
+    if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
+      return true;
+    } else if ("restart".equalsIgnoreCase(v)) {
+      return false;
+    } else {
+      PluginLoader.log.warn(String.format(
+          "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
+          getName(), v));
+      return false;
+    }
+  }
+
+  void start(PluginGuiceEnvironment env) throws Exception {
+    RequestContext oldContext = env.enter(this);
+    try {
+      startPlugin(env);
+    } finally {
+      env.exit(oldContext);
+    }
+  }
+
+  private void startPlugin(PluginGuiceEnvironment env) throws Exception {
+    Injector root = newRootInjector(env);
+    manager = new LifecycleManager();
+
+    AutoRegisterModules auto = null;
+    if (sysModule == null && sshModule == null && httpModule == null) {
+      auto = new AutoRegisterModules(getName(), env, jarFile, classLoader);
+      auto.discover();
+    }
+
+    if (sysModule != null) {
+      sysInjector = root.createChildInjector(root.getInstance(sysModule));
+      manager.add(sysInjector);
+    } else if (auto != null && auto.sysModule != null) {
+      sysInjector = root.createChildInjector(auto.sysModule);
+      manager.add(sysInjector);
+    } else {
+      sysInjector = root;
+    }
+
+    if (env.hasSshModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (getApiType() == ApiType.PLUGIN) {
+        modules.add(env.getSshModule());
+      }
+      if (sshModule != null) {
+        modules.add(sysInjector.getInstance(sshModule));
+        sshInjector = sysInjector.createChildInjector(modules);
+        manager.add(sshInjector);
+      } else if (auto != null && auto.sshModule != null) {
+        modules.add(auto.sshModule);
+        sshInjector = sysInjector.createChildInjector(modules);
+        manager.add(sshInjector);
+      }
+    }
+
+    if (env.hasHttpModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (getApiType() == ApiType.PLUGIN) {
+        modules.add(env.getHttpModule());
+      }
+      if (httpModule != null) {
+        modules.add(sysInjector.getInstance(httpModule));
+        httpInjector = sysInjector.createChildInjector(modules);
+        manager.add(httpInjector);
+      } else if (auto != null && auto.httpModule != null) {
+        modules.add(auto.httpModule);
+        httpInjector = sysInjector.createChildInjector(modules);
+        manager.add(httpInjector);
+      }
+    }
+
+    manager.start();
+  }
+
+  private Injector newRootInjector(final PluginGuiceEnvironment env) {
+    List<Module> modules = Lists.newArrayListWithCapacity(4);
+    if (getApiType() == ApiType.PLUGIN) {
+      modules.add(env.getSysModule());
+    }
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(PluginUser.class).toInstance(getPluginUser());
+        bind(String.class)
+          .annotatedWith(PluginName.class)
+          .toInstance(getName());
+
+        bind(File.class)
+          .annotatedWith(PluginData.class)
+          .toProvider(new Provider<File>() {
+            private volatile boolean ready;
+
+            @Override
+            public File get() {
+              if (!ready) {
+                synchronized (dataDir) {
+                  if (!dataDir.exists() && !dataDir.mkdirs()) {
+                    throw new ProvisionException(String.format(
+                        "Cannot create %s for plugin %s",
+                        dataDir.getAbsolutePath(), getName()));
+                  }
+                  ready = true;
+                }
+              }
+              return dataDir;
+            }
+          });
+      }
+    });
+    return Guice.createInjector(modules);
+  }
+
+  void stop(PluginGuiceEnvironment env) {
+    if (manager != null) {
+      RequestContext oldContext = env.enter(this);
+      try {
+        manager.stop();
+      } finally {
+        env.exit(oldContext);
+      }
+      manager = null;
+      sysInjector = null;
+      sshInjector = null;
+      httpInjector = null;
+    }
+  }
+
+  public JarFile getJarFile() {
+    return jarFile;
+  }
+
+  public Injector getSysInjector() {
+    return sysInjector;
+  }
+
+  @Nullable
+  public Injector getSshInjector() {
+    return sshInjector;
+  }
+
+  @Nullable
+  public Injector getHttpInjector() {
+    return httpInjector;
+  }
+
+  public void add(RegistrationHandle handle) {
+    if (manager != null) {
+      if (handle instanceof ReloadableRegistrationHandle) {
+        if (reloadableHandles == null) {
+          reloadableHandles = Lists.newArrayList();
+        }
+        reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+      }
+      manager.add(handle);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index cb65e8c..ca0f7ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -97,7 +97,7 @@
     });
 
     if (!format.isJson()) {
-      stdout.format("%-30s %-10s %-8s\n", "Name", "Version", "Status");
+      stdout.format("%-30s %-10s %-8s %s\n", "Name", "Version", "Status", "File");
       stdout.print("-------------------------------------------------------------------------------\n");
     }
 
@@ -106,9 +106,10 @@
       if (format.isJson()) {
         output.put(p.getName(), info);
       } else {
-        stdout.format("%-30s %-10s %-8s\n", p.getName(),
+        stdout.format("%-30s %-10s %-8s %s\n", p.getName(),
             Strings.nullToEmpty(info.version),
-            p.isDisabled() ? "DISABLED" : "ENABLED");
+            p.isDisabled() ? "DISABLED" : "ENABLED",
+            p.getSrcFile().getName());
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 8e7192e..d9a2c0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -16,19 +16,12 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.annotations.PluginData;
-import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.server.PluginUser;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
 import com.google.inject.Injector;
-import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
@@ -39,9 +32,7 @@
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
-import javax.annotation.Nullable;
-
-public class Plugin {
+public abstract class Plugin {
   public static enum ApiType {
     EXTENSION, PLUGIN, JS;
   }
@@ -61,13 +52,6 @@
     }
   }
 
-  static {
-    // Guice logs warnings about multiple injectors being created.
-    // Silence this in case HTTP plugins are used.
-    java.util.logging.Logger.getLogger("com.google.inject.servlet.GuiceFilter")
-        .setLevel(java.util.logging.Level.OFF);
-  }
-
   static ApiType getApiType(Manifest manifest) throws InvalidPluginException {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ApiType");
@@ -83,238 +67,72 @@
     }
   }
 
-  private final CacheKey cacheKey;
   private final String name;
-  private final PluginUser pluginUser;
-  private final File srcJar;
-  private final FileSnapshot snapshot;
-  private final JarFile jarFile;
-  private final Manifest manifest;
-  private final File dataDir;
+  private final File srcFile;
   private final ApiType apiType;
-  private final ClassLoader classLoader;
   private final boolean disabled;
-  private Class<? extends Module> sysModule;
-  private Class<? extends Module> sshModule;
-  private Class<? extends Module> httpModule;
+  private final CacheKey cacheKey;
+  private final PluginUser pluginUser;
+  private final FileSnapshot snapshot;
 
-  private Injector sysInjector;
-  private Injector sshInjector;
-  private Injector httpInjector;
-  private LifecycleManager manager;
+  protected LifecycleManager manager;
+
   private List<ReloadableRegistrationHandle<?>> reloadableHandles;
 
   public Plugin(String name,
+      File srcFile,
       PluginUser pluginUser,
-      File srcJar,
       FileSnapshot snapshot,
-      JarFile jarFile,
-      Manifest manifest,
-      File dataDir,
-      ApiType apiType,
-      ClassLoader classLoader,
-      @Nullable Class<? extends Module> sysModule,
-      @Nullable Class<? extends Module> sshModule,
-      @Nullable Class<? extends Module> httpModule) {
-    this.cacheKey = new CacheKey(name);
-    this.pluginUser = pluginUser;
+      ApiType apiType) {
     this.name = name;
-    this.srcJar = srcJar;
-    this.snapshot = snapshot;
-    this.jarFile = jarFile;
-    this.manifest = manifest;
-    this.dataDir = dataDir;
+    this.srcFile = srcFile;
     this.apiType = apiType;
-    this.classLoader = classLoader;
-    this.disabled = srcJar.getName().endsWith(".disabled");
-    this.sysModule = sysModule;
-    this.sshModule = sshModule;
-    this.httpModule = httpModule;
+    this.snapshot = snapshot;
+    this.pluginUser = pluginUser;
+    this.cacheKey = new Plugin.CacheKey(name);
+    this.disabled = srcFile.getName().endsWith(".disabled");
   }
 
-  File getSrcJar() {
-    return srcJar;
+  File getSrcFile() {
+    return srcFile;
   }
 
   PluginUser getPluginUser() {
     return pluginUser;
   }
 
-  public CacheKey getCacheKey() {
-    return cacheKey;
-  }
-
   public String getName() {
     return name;
   }
 
   @Nullable
-  public String getVersion() {
-    Attributes main = manifest.getMainAttributes();
-    return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
-  }
+  public abstract String getVersion();
 
   public ApiType getApiType() {
     return apiType;
   }
 
-  boolean canReload() {
-    Attributes main = manifest.getMainAttributes();
-    String v = main.getValue("Gerrit-ReloadMode");
-    if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
-      return true;
-    } else if ("restart".equalsIgnoreCase(v)) {
-      return false;
-    } else {
-      PluginLoader.log.warn(String.format(
-          "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
-          name, v));
-      return false;
-    }
-  }
-
-  boolean isModified(File jar) {
-    return snapshot.lastModified() != jar.lastModified();
+  public Plugin.CacheKey getCacheKey() {
+    return cacheKey;
   }
 
   public boolean isDisabled() {
     return disabled;
   }
 
-  void start(PluginGuiceEnvironment env) throws Exception {
-    RequestContext oldContext = env.enter(this);
-    try {
-      startPlugin(env);
-    } finally {
-      env.exit(oldContext);
-    }
-  }
+  abstract void start(PluginGuiceEnvironment env) throws Exception;
 
-  private void startPlugin(PluginGuiceEnvironment env) throws Exception {
-    Injector root = newRootInjector(env);
-    manager = new LifecycleManager();
+  abstract void stop(PluginGuiceEnvironment env);
 
-    AutoRegisterModules auto = null;
-    if (sysModule == null && sshModule == null && httpModule == null) {
-      auto = new AutoRegisterModules(name, env, jarFile, classLoader);
-      auto.discover();
-    }
+  public abstract JarFile getJarFile();
 
-    if (sysModule != null) {
-      sysInjector = root.createChildInjector(root.getInstance(sysModule));
-      manager.add(sysInjector);
-    } else if (auto != null && auto.sysModule != null) {
-      sysInjector = root.createChildInjector(auto.sysModule);
-      manager.add(sysInjector);
-    } else {
-      sysInjector = root;
-    }
-
-    if (env.hasSshModule()) {
-      List<Module> modules = Lists.newLinkedList();
-      if (apiType == ApiType.PLUGIN) {
-        modules.add(env.getSshModule());
-      }
-      if (sshModule != null) {
-        modules.add(sysInjector.getInstance(sshModule));
-        sshInjector = sysInjector.createChildInjector(modules);
-        manager.add(sshInjector);
-      } else if (auto != null && auto.sshModule != null) {
-        modules.add(auto.sshModule);
-        sshInjector = sysInjector.createChildInjector(modules);
-        manager.add(sshInjector);
-      }
-    }
-
-    if (env.hasHttpModule()) {
-      List<Module> modules = Lists.newLinkedList();
-      if (apiType == ApiType.PLUGIN) {
-        modules.add(env.getHttpModule());
-      }
-      if (httpModule != null) {
-        modules.add(sysInjector.getInstance(httpModule));
-        httpInjector = sysInjector.createChildInjector(modules);
-        manager.add(httpInjector);
-      } else if (auto != null && auto.httpModule != null) {
-        modules.add(auto.httpModule);
-        httpInjector = sysInjector.createChildInjector(modules);
-        manager.add(httpInjector);
-      }
-    }
-
-    manager.start();
-  }
-
-  private Injector newRootInjector(final PluginGuiceEnvironment env) {
-    List<Module> modules = Lists.newArrayListWithCapacity(4);
-    if (apiType == ApiType.PLUGIN) {
-      modules.add(env.getSysModule());
-    }
-    modules.add(new AbstractModule() {
-      @Override
-      protected void configure() {
-        bind(PluginUser.class).toInstance(pluginUser);
-        bind(String.class)
-          .annotatedWith(PluginName.class)
-          .toInstance(name);
-
-        bind(File.class)
-          .annotatedWith(PluginData.class)
-          .toProvider(new Provider<File>() {
-            private volatile boolean ready;
-
-            @Override
-            public File get() {
-              if (!ready) {
-                synchronized (dataDir) {
-                  if (!dataDir.exists() && !dataDir.mkdirs()) {
-                    throw new ProvisionException(String.format(
-                        "Cannot create %s for plugin %s",
-                        dataDir.getAbsolutePath(), name));
-                  }
-                  ready = true;
-                }
-              }
-              return dataDir;
-            }
-          });
-      }
-    });
-    return Guice.createInjector(modules);
-  }
-
-  void stop(PluginGuiceEnvironment env) {
-    if (manager != null) {
-      RequestContext oldContext = env.enter(this);
-      try {
-        manager.stop();
-      } finally {
-        env.exit(oldContext);
-      }
-      manager = null;
-      sysInjector = null;
-      sshInjector = null;
-      httpInjector = null;
-    }
-  }
-
-  public JarFile getJarFile() {
-    return jarFile;
-  }
-
-  public Injector getSysInjector() {
-    return sysInjector;
-  }
+  public abstract Injector getSysInjector();
 
   @Nullable
-  public Injector getSshInjector() {
-    return sshInjector;
-  }
+  public abstract Injector getSshInjector();
 
   @Nullable
-  public Injector getHttpInjector() {
-    return httpInjector;
-  }
+  public abstract Injector getHttpInjector();
 
   public void add(RegistrationHandle handle) {
     if (manager != null) {
@@ -339,4 +157,10 @@
   public String toString() {
     return "Plugin [" + name + "]";
   }
+
+  abstract boolean canReload();
+
+  boolean isModified(File jar) {
+    return snapshot.lastModified() != jar.lastModified();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
index 7081d70..606d5da 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -52,7 +53,7 @@
       self = null;
 
       if (0 < left) {
-        long waiting = System.currentTimeMillis() - start;
+        long waiting = TimeUtil.nowMs() - start;
         PluginLoader.log.warn(String.format(
             "%d plugins still waiting to be reclaimed after %d minutes",
             pending,
@@ -76,7 +77,7 @@
 
   synchronized void clean(int expect) {
     if (self == null && pending == 0) {
-      start = System.currentTimeMillis();
+      start = TimeUtil.nowMs();
     }
     pending = expect;
     ensureScheduled();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 387ffa4..ae90d5b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -54,7 +55,6 @@
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 
-import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 035592c..7569744 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -15,9 +15,14 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.annotations.PluginName;
@@ -49,6 +54,7 @@
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
@@ -129,34 +135,41 @@
     }
   }
 
-  public void installPluginFromStream(String name, InputStream in)
+  public void installPluginFromStream(String originalName, InputStream in)
       throws IOException, PluginInstallException {
-    if (!name.endsWith(".jar")) {
-      name += ".jar";
+    String fileName = originalName;
+    if (!fileName.endsWith(".jar")) {
+      fileName += ".jar";
+    }
+    File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
+    String name = Objects.firstNonNull(getGerritPluginName(tmp),
+        nameOf(fileName));
+    if (!originalName.equals(name)) {
+      log.warn(String.format("Plugin provides its own name: <%s>,"
+          + " use it instead of the input name: <%s>",
+          name, originalName));
     }
 
-    File jar = new File(pluginsDir, name);
-    name = nameOf(jar);
-
-    File old = new File(pluginsDir, ".last_" + name + ".zip");
-    File tmp = asTemp(in, ".next_" + name, ".zip", pluginsDir);
+    File dst = new File(pluginsDir, name + ".jar");
     synchronized (this) {
       Plugin active = running.get(name);
       if (active != null) {
-        log.info(String.format("Replacing plugin %s", name));
+        fileName = active.getSrcFile().getName();
+        log.info(String.format("Replacing plugin %s", active.getName()));
+        File old = new File(pluginsDir, ".last_" + fileName);
         old.delete();
-        jar.renameTo(old);
+        active.getSrcFile().renameTo(old);
       }
 
-      new File(pluginsDir, name + ".jar.disabled").delete();
-      tmp.renameTo(jar);
+      new File(pluginsDir, fileName + ".disabled").delete();
+      tmp.renameTo(dst);
       try {
-        runPlugin(name, jar, active);
+        Plugin plugin = runPlugin(name, dst, active);
         if (active == null) {
-          log.info(String.format("Installed plugin %s", name));
+          log.info(String.format("Installed plugin %s", plugin.getName()));
         }
       } catch (PluginInstallException e) {
-        jar.delete();
+        dst.delete();
         throw e;
       }
 
@@ -166,6 +179,9 @@
 
   public static File storeInTemp(String pluginName, InputStream in,
       SitePaths sitePaths) throws IOException {
+    if (!sitePaths.tmp_dir.exists()) {
+      sitePaths.tmp_dir.mkdirs();
+    }
     return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
   }
 
@@ -211,9 +227,9 @@
           continue;
         }
 
-        log.info(String.format("Disabling plugin %s", name));
-        File off = new File(pluginsDir, active.getName() + ".jar.disabled");
-        active.getSrcJar().renameTo(off);
+        log.info(String.format("Disabling plugin %s", active.getName()));
+        File off = new File(active.getSrcFile() + ".disabled");
+        active.getSrcFile().renameTo(off);
 
         unloadPlugin(active);
         try {
@@ -222,7 +238,8 @@
           disabled.put(name, offPlugin);
         } catch (Throwable e) {
           // This shouldn't happen, as the plugin was loaded earlier.
-          log.warn(String.format("Cannot load disabled plugin %s", name),
+          log.warn(String.format(
+              "Cannot load disabled plugin %s", active.getName()),
               e.getCause());
         }
       }
@@ -239,8 +256,12 @@
         }
 
         log.info(String.format("Enabling plugin %s", name));
-        File on = new File(pluginsDir, off.getName() + ".jar");
-        off.getSrcJar().renameTo(on);
+        String n = off.getSrcFile().getName();
+        if (n.endsWith(".disabled")) {
+          n = n.substring(0, n.lastIndexOf('.'));
+        }
+        File on = new File(pluginsDir, n);
+        off.getSrcFile().renameTo(on);
 
         disabled.remove(name);
         runPlugin(name, on, null);
@@ -303,7 +324,7 @@
         String name = active.getName();
         try {
           log.info(String.format("Reloading plugin %s", name));
-          runPlugin(name, active.getSrcJar(), active);
+          runPlugin(name, active.getSrcFile(), active);
         } catch (PluginInstallException e) {
           log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
           throw e;
@@ -315,16 +336,16 @@
   }
 
   public synchronized void rescan() {
-    List<File> jars = scanJarsInPluginsDirectory();
-    stopRemovedPlugins(jars);
-    dropRemovedDisabledPlugins(jars);
+    Multimap<String, File> jars = prunePlugins(pluginsDir);
+    if (jars.isEmpty()) {
+      return;
+    }
 
-    for (File jar : jars) {
-      if (jar.getName().endsWith(".disabled")) {
-        continue;
-      }
+    syncDisabledPlugins(jars);
 
-      String name = nameOf(jar);
+    Map<String, File> activePlugins = filterDisabled(jars);
+    for (String name : activePlugins.keySet()) {
+      File jar = activePlugins.get(name);
       FileSnapshot brokenTime = broken.get(name);
       if (brokenTime != null && !brokenTime.isModified(jar)) {
         continue;
@@ -336,13 +357,15 @@
       }
 
       if (active != null) {
-        log.info(String.format("Reloading plugin %s", name));
+        log.info(String.format("Reloading plugin %s, version %s",
+            active.getName(), active.getVersion()));
       }
 
       try {
         Plugin loadedPlugin = runPlugin(name, jar, active);
         if (active == null && !loadedPlugin.isDisabled()) {
-          log.info(String.format("Loaded plugin %s", name));
+          log.info(String.format("Loaded plugin %s, version %s",
+              loadedPlugin.getName(), loadedPlugin.getVersion()));
         }
       } catch (PluginInstallException e) {
         log.warn(String.format("Cannot load plugin %s", name), e.getCause());
@@ -352,6 +375,11 @@
     cleanInBackground();
   }
 
+  private void syncDisabledPlugins(Multimap<String, File> jars) {
+    stopRemovedPlugins(jars);
+    dropRemovedDisabledPlugins(jars);
+  }
+
   private Plugin runPlugin(String name, File jar, Plugin oldPlugin)
       throws PluginInstallException {
     FileSnapshot snapshot = FileSnapshot.save(jar);
@@ -385,23 +413,27 @@
     }
   }
 
-  private void stopRemovedPlugins(List<File> jars) {
+  private void stopRemovedPlugins(Multimap<String, File> jars) {
     Set<String> unload = Sets.newHashSet(running.keySet());
-    for (File jar : jars) {
-      if (!jar.getName().endsWith(".disabled")) {
-        unload.remove(nameOf(jar));
+    for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
+      for (File file : entry.getValue()) {
+        if (!file.getName().endsWith(".disabled")) {
+          unload.remove(entry.getKey());
+        }
       }
     }
-    for (String name : unload){
+    for (String name : unload) {
       unloadPlugin(running.get(name));
     }
   }
 
-  private void dropRemovedDisabledPlugins(List<File> jars) {
+  private void dropRemovedDisabledPlugins(Multimap<String, File> jars) {
     Set<String> unload = Sets.newHashSet(disabled.keySet());
-    for (File jar : jars) {
-      if (jar.getName().endsWith(".disabled")) {
-        unload.remove(nameOf(jar));
+    for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
+      for (File file : entry.getValue()) {
+        if (file.getName().endsWith(".disabled")) {
+          unload.remove(entry.getKey());
+        }
       }
     }
     for (String name : unload) {
@@ -428,8 +460,11 @@
     }
   }
 
-  private static String nameOf(File jar) {
-    String name = jar.getName();
+  public static String nameOf(File jar) {
+    return nameOf(jar.getName());
+  }
+
+  private static String nameOf(String name) {
     if (name.endsWith(".disabled")) {
       name = name.substring(0, name.lastIndexOf('.'));
     }
@@ -469,7 +504,7 @@
       Class<? extends Module> sysModule = load(sysName, pluginLoader);
       Class<? extends Module> sshModule = load(sshName, pluginLoader);
       Class<? extends Module> httpModule = load(httpName, pluginLoader);
-      Plugin plugin = new Plugin(name, pluginUserFactory.create(name),
+      Plugin plugin = new JarPlugin(name, pluginUserFactory.create(name),
           srcJar, snapshot,
           jarFile, manifest,
           new File(dataDir, name), type, pluginLoader,
@@ -503,7 +538,7 @@
     return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
   }
 
-  private Class<? extends Module> load(String name, ClassLoader pluginLoader)
+  private static Class<? extends Module> load(String name, ClassLoader pluginLoader)
       throws ClassNotFoundException {
     if (Strings.isNullOrEmpty(name)) {
       return null;
@@ -520,7 +555,74 @@
     return clazz;
   }
 
-  private List<File> scanJarsInPluginsDirectory() {
+  // Only one active plugin per plugin name can exist for each plugin name.
+  // Filter out disabled plugins and transform the multimap to a map
+  private static Map<String, File> filterDisabled(
+      Multimap<String, File> jars) {
+    Map<String, File> activePlugins = Maps.newHashMapWithExpectedSize(
+        jars.keys().size());
+    for (String name : jars.keys()) {
+      for (File jar : jars.asMap().get(name)) {
+        if (!jar.getName().endsWith(".disabled")) {
+          assert(!activePlugins.containsKey(name));
+          activePlugins.put(name, jar);
+        }
+      }
+    }
+    return activePlugins;
+  }
+
+  // Scan the $site_path/plugins directory and fetch all files that end
+  // with *.jar. The Key in returned multimap is the plugin name. Values are
+  // the files. Plugins can optionally provide their name in MANIFEST file.
+  // If multiple plugin files provide the same plugin name, then only
+  // the first plugin remains active and all other plugins with the same
+  // name are disabled.
+  private static Multimap<String, File> prunePlugins(File pluginsDir) {
+    List<File> jars = scanJarsInPluginsDirectory(pluginsDir);
+    Multimap<String, File> map;
+    try {
+      map = asMultimap(jars);
+      for (String plugin : map.keySet()) {
+        Collection<File> files = map.asMap().get(plugin);
+        if (files.size() == 1) {
+          continue;
+        }
+        // retrieve enabled plugins
+        Iterable<File> enabled = filterDisabledPlugins(
+            files);
+        // If we have only one (the winner) plugin, nothing to do
+        if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
+          continue;
+        }
+        File winner = Iterables.getFirst(enabled, null);
+        assert(winner != null);
+        // Disable all loser plugins by renaming their file names to
+        // "file.disabled" and replace the disabled files in the multimap.
+        Collection<File> elementsToRemove = Lists.newArrayList();
+        Collection<File> elementsToAdd = Lists.newArrayList();
+        for (File loser : Iterables.skip(enabled, 1)) {
+          log.warn(String.format("Plugin <%s> was disabled, because"
+               + " another plugin <%s>"
+               + " with the same name <%s> already exists",
+               loser, winner, plugin));
+          File disabledPlugin = new File(loser + ".disabled");
+          elementsToAdd.add(disabledPlugin);
+          elementsToRemove.add(loser);
+          loser.renameTo(disabledPlugin);
+        }
+        Iterables.removeAll(files, elementsToRemove);
+        Iterables.addAll(files, elementsToAdd);
+      }
+    } catch (IOException e) {
+      log.warn("Cannot prune plugin list",
+          e.getCause());
+      return LinkedHashMultimap.create();
+    }
+    return map;
+  }
+
+  private static List<File> scanJarsInPluginsDirectory(File pluginsDir) {
     if (pluginsDir == null || !pluginsDir.exists()) {
       return Collections.emptyList();
     }
@@ -529,6 +631,8 @@
       public boolean accept(File pathname) {
         String n = pathname.getName();
         return (n.endsWith(".jar") || n.endsWith(".jar.disabled"))
+            && !n.startsWith(".last_")
+            && !n.startsWith(".next_")
             && pathname.isFile();
       }
     });
@@ -538,4 +642,34 @@
     }
     return Arrays.asList(matches);
   }
+
+  private static Iterable<File> filterDisabledPlugins(
+      Collection<File> files) {
+    return Iterables.filter(files, new Predicate<File>() {
+      @Override
+      public boolean apply(File file) {
+        return !file.getName().endsWith(".disabled");
+      }
+    });
+  }
+
+  public static String getGerritPluginName(File srcFile) throws IOException {
+    JarFile jarFile = new JarFile(srcFile);
+    try {
+      return jarFile.getManifest().getMainAttributes()
+          .getValue("Gerrit-PluginName");
+    } finally {
+      jarFile.close();
+    }
+  }
+
+  private static Multimap<String, File> asMultimap(List<File> plugins)
+      throws IOException {
+    Multimap<String, File> map = LinkedHashMultimap.create();
+    for (File srcFile : plugins) {
+      map.put(Objects.firstNonNull(getGerritPluginName(srcFile),
+          nameOf(srcFile)), srcFile);
+    }
+    return map;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
index 4cdafb3..bd1c3c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -14,21 +14,17 @@
 
 package com.google.gerrit.server.plugins;
 
-import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
-
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.AbstractModule;
 
-public class PluginModule extends RestApiModule {
+public class PluginModule extends AbstractModule {
   @Override
   protected void configure() {
     bind(ServerInformationImpl.class);
     bind(ServerInformation.class).to(ServerInformationImpl.class);
 
     bind(PluginCleanerTask.class);
-    bind(PluginsCollection.class);
     bind(PluginGuiceEnvironment.class);
     bind(PluginLoader.class);
     bind(CopyConfigModule.class);
@@ -38,13 +34,5 @@
         listener().to(PluginLoader.class);
       }
     });
-
-    DynamicMap.mapOf(binder(), PLUGIN_KIND);
-    put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
-    delete(PLUGIN_KIND).to(DisablePlugin.class);
-    get(PLUGIN_KIND, "status").to(GetStatus.class);
-    post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
-    post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
-    post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginRestApiModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
new file mode 100644
index 0000000..6b52a59
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
@@ -0,0 +1,35 @@
+// 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.plugins;
+
+import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class PluginRestApiModule extends RestApiModule {
+  @Override
+  protected void configure() {
+    install(new PluginModule());
+    bind(PluginsCollection.class);
+    DynamicMap.mapOf(binder(), PLUGIN_KIND);
+    put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
+    delete(PLUGIN_KIND).to(DisablePlugin.class);
+    get(PLUGIN_KIND, "status").to(GetStatus.class);
+    post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
+    post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
+    post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
new file mode 100644
index 0000000..6681d94
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -0,0 +1,44 @@
+// 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.project;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.inject.TypeLiteral;
+
+public class BranchResource extends ProjectResource {
+  public static final TypeLiteral<RestView<BranchResource>> BRANCH_KIND =
+      new TypeLiteral<RestView<BranchResource>>() {};
+
+  private final BranchInfo branchInfo;
+
+  public BranchResource(ProjectControl control, BranchInfo branchInfo) {
+    super(control);
+    this.branchInfo = branchInfo;
+  }
+
+  public BranchInfo getBranchInfo() {
+    return branchInfo;
+  }
+
+  public Branch.NameKey getBranchKey() {
+    return new Branch.NameKey(getNameKey(), branchInfo.ref);
+  }
+
+  public String getRef() {
+    return branchInfo.ref;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
new file mode 100644
index 0000000..8ae07de
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -0,0 +1,79 @@
+// 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.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Constants;
+
+import java.io.IOException;
+import java.util.List;
+
+public class BranchesCollection implements
+    ChildCollection<ProjectResource, BranchResource>,
+    AcceptsCreate<ProjectResource> {
+  private final DynamicMap<RestView<BranchResource>> views;
+  private final Provider<ListBranches> list;
+  private final CreateBranch.Factory createBranchFactory;
+
+  @Inject
+  BranchesCollection(DynamicMap<RestView<BranchResource>> views,
+      Provider<ListBranches> list, CreateBranch.Factory createBranchFactory) {
+    this.views = views;
+    this.list = list;
+    this.createBranchFactory = createBranchFactory;
+  }
+
+  @Override
+  public RestView<ProjectResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public BranchResource parse(ProjectResource parent, IdString id)
+      throws ResourceNotFoundException, IOException {
+    String branchName = id.get();
+    if (!branchName.startsWith(Constants.R_REFS)
+        && !branchName.equals(Constants.HEAD)) {
+      branchName = Constants.R_HEADS + branchName;
+    }
+    List<BranchInfo> branches = list.get().apply(parent);
+    for (BranchInfo b : branches) {
+      if (branchName.equals(b.ref)) {
+        return new BranchResource(parent.getControl(), b);
+      }
+    }
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public DynamicMap<RestView<BranchResource>> views() {
+    return views;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateBranch create(ProjectResource parent, IdString name) {
+    return createBranchFactory.create(name.get());
+  }
+}
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 6397b96..698a0ac 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,8 +14,12 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -42,12 +46,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 
 /** Access control management for a user accessing a single change. */
 public class ChangeControl {
@@ -56,10 +59,12 @@
 
   public static class GenericFactory {
     private final ProjectControl.GenericFactory projectControl;
+    private final Provider<ReviewDb> db;
 
     @Inject
-    GenericFactory(ProjectControl.GenericFactory p) {
+    GenericFactory(ProjectControl.GenericFactory p, Provider<ReviewDb> d) {
       projectControl = p;
+      db = d;
     }
 
     public ChangeControl controlFor(Change change, CurrentUser user)
@@ -69,8 +74,43 @@
         return projectControl.controlFor(projectKey, user).controlFor(change);
       } catch (NoSuchProjectException e) {
         throw new NoSuchChangeException(change.getId(), e);
+      } catch (IOException e) {
+        // TODO: propagate this exception
+        throw new NoSuchChangeException(change.getId(), e);
       }
     }
+
+    public ChangeControl controlFor(Change.Id id, CurrentUser user)
+        throws NoSuchChangeException {
+      final Change change;
+      try {
+        change = db.get().changes().get(id);
+        if (change == null) {
+          throw new NoSuchChangeException(id);
+        }
+      } catch (OrmException e) {
+        throw new NoSuchChangeException(id, e);
+      }
+      return controlFor(change, user);
+    }
+
+    public ChangeControl validateFor(Change change, CurrentUser user)
+        throws NoSuchChangeException, OrmException {
+      ChangeControl c = controlFor(change, user);
+      if (!c.isVisible(db.get())) {
+        throw new NoSuchChangeException(c.getChange().getId());
+      }
+      return c;
+    }
+
+    public ChangeControl validateFor(Change.Id id, CurrentUser user)
+        throws NoSuchChangeException, OrmException {
+      ChangeControl c = controlFor(id, user);
+      if (!c.isVisible(db.get())) {
+        throw new NoSuchChangeException(c.getChange().getId());
+      }
+      return c;
+    }
   }
 
   public static class Factory {
@@ -213,9 +253,28 @@
         && getRefControl().canUpload(); // as long as you can upload too
   }
 
-  /** All available label types for this project. */
+  /** All available label types for this change. */
   public LabelTypes getLabelTypes() {
-    return getProjectControl().getLabelTypes();
+    String destBranch = getChange().getDest().get();
+    List<LabelType> all = getProjectControl().getLabelTypes().getLabelTypes();
+
+    List<LabelType> r = Lists.newArrayListWithCapacity(all.size());
+    for (LabelType l : all) {
+      List<String> refs = l.getRefPatterns();
+      if (refs == null) {
+        r.add(l);
+      } else {
+        for (String refPattern : refs) {
+          if (RefConfigSection.isValid(refPattern)
+              && match(destBranch, refPattern)) {
+            r.add(l);
+            break;
+          }
+        }
+      }
+    }
+
+    return new LabelTypes(r);
   }
 
   /** All value ranges of any allowed label permission. */
@@ -235,7 +294,7 @@
 
   /** Is this user the owner of the change? */
   public boolean isOwner() {
-    if (getCurrentUser() instanceof IdentifiedUser) {
+    if (getCurrentUser().isIdentifiedUser()) {
       final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
       return i.getAccountId().equals(change.getOwner());
     }
@@ -250,7 +309,7 @@
   /** Is this user a reviewer for the change? */
   public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
       throws OrmException {
-    if (getCurrentUser() instanceof IdentifiedUser) {
+    if (getCurrentUser().isIdentifiedUser()) {
       final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
       Iterable<PatchSetApproval> results;
       if (cd != null) {
@@ -276,7 +335,7 @@
     if (getChange().getStatus().isOpen()) {
       // A user can always remove themselves.
       //
-      if (getCurrentUser() instanceof IdentifiedUser) {
+      if (getCurrentUser().isIdentifiedUser()) {
         final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
         if (i.getAccountId().equals(reviewer)) {
           return true; // can remove self
@@ -374,6 +433,11 @@
     return resultsToSubmitRecord(evaluator.getSubmitRule(), results);
   }
 
+  private boolean match(String destBranch, String refPattern) {
+    return RefPatternMatcher.getMatcher(refPattern).match(destBranch,
+        this.getRefControl().getCurrentUser().getUserName());
+  }
+
   private List<SubmitRecord> cannotSubmitDraft(ReviewDb db, PatchSet patchSet,
       ChangeData cd) {
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
new file mode 100644
index 0000000..2a386a4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
@@ -0,0 +1,42 @@
+// 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.project;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class ChildProjectResource extends ProjectResource {
+  public static final TypeLiteral<RestView<ChildProjectResource>> CHILD_PROJECT_KIND =
+      new TypeLiteral<RestView<ChildProjectResource>>() {};
+
+  private final ProjectControl child;
+
+  ChildProjectResource(ProjectResource project, ProjectControl child) {
+    super(project);
+    this.child = child;
+  }
+
+  public ProjectControl getChild() {
+    return child;
+  }
+
+  public boolean isDirectChild() {
+    ProjectState parent =
+        Iterables.getFirst(child.getProjectState().parents(), null);
+    return parent != null
+        && getNameKey().equals(parent.getProject().getNameKey());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
new file mode 100644
index 0000000..27f1648
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
@@ -0,0 +1,67 @@
+// 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.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class ChildProjectsCollection implements
+    ChildCollection<ProjectResource, ChildProjectResource> {
+  private final Provider<ListChildProjects> list;
+  private final Provider<ProjectsCollection> projectsCollection;
+  private final DynamicMap<RestView<ChildProjectResource>> views;
+
+  @Inject
+  ChildProjectsCollection(Provider<ListChildProjects> list,
+      Provider<ProjectsCollection> projectsCollection,
+      DynamicMap<RestView<ChildProjectResource>> views) {
+    this.list = list;
+    this.projectsCollection = projectsCollection;
+    this.views = views;
+  }
+
+  @Override
+  public RestView<ProjectResource> list() throws ResourceNotFoundException,
+      AuthException {
+    return list.get();
+  }
+
+  @Override
+  public ChildProjectResource parse(ProjectResource parent, IdString id)
+      throws ResourceNotFoundException, IOException {
+    ProjectResource p =
+        projectsCollection.get().parse(TopLevelResource.INSTANCE, id);
+    for (ProjectState pp : p.getControl().getProjectState().parents()) {
+      if (parent.getNameKey().equals(pp.getProject().getNameKey())) {
+        return new ChildProjectResource(parent, p.getControl());
+      }
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<ChildProjectResource>> views() {
+    return views;
+  }
+}
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
new file mode 100644
index 0000000..9b9b517
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -0,0 +1,126 @@
+// 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.project;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.actions.ActionInfo;
+import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.inject.util.Providers;
+
+import java.util.Map;
+
+public class ConfigInfo {
+  public final String kind = "gerritcodereview#project_config";
+
+  public String description;
+  public InheritedBooleanInfo useContributorAgreements;
+  public InheritedBooleanInfo useContentMerge;
+  public InheritedBooleanInfo useSignedOffBy;
+  public InheritedBooleanInfo requireChangeId;
+  public MaxObjectSizeLimitInfo maxObjectSizeLimit;
+  public SubmitType submitType;
+  public Project.State state;
+  public Map<String, ActionInfo> actions;
+
+  public Map<String, CommentLinkInfo> commentlinks;
+  public ThemeInfo theme;
+
+  public ConfigInfo(ProjectControl control,
+      TransferConfig config,
+      DynamicMap<RestView<ProjectResource>> views) {
+    ProjectState projectState = control.getProjectState();
+    Project p = control.getProject();
+    this.description = Strings.emptyToNull(p.getDescription());
+
+    InheritedBooleanInfo useContributorAgreements =
+        new InheritedBooleanInfo();
+    InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
+    InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
+    InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
+
+    useContributorAgreements.value = projectState.isUseContributorAgreements();
+    useSignedOffBy.value = projectState.isUseSignedOffBy();
+    useContentMerge.value = projectState.isUseContentMerge();
+    requireChangeId.value = projectState.isRequireChangeID();
+
+    useContributorAgreements.configuredValue =
+        p.getUseContributorAgreements();
+    useSignedOffBy.configuredValue = p.getUseSignedOffBy();
+    useContentMerge.configuredValue = p.getUseContentMerge();
+    requireChangeId.configuredValue = p.getRequireChangeID();
+
+    ProjectState parentState = Iterables.getFirst(projectState
+        .parents(), null);
+    if (parentState != null) {
+      useContributorAgreements.inheritedValue =
+          parentState.isUseContributorAgreements();
+      useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
+      useContentMerge.inheritedValue = parentState.isUseContentMerge();
+      requireChangeId.inheritedValue = parentState.isRequireChangeID();
+    }
+
+    this.useContributorAgreements = useContributorAgreements;
+    this.useSignedOffBy = useSignedOffBy;
+    this.useContentMerge = useContentMerge;
+    this.requireChangeId = requireChangeId;
+
+    MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
+    maxObjectSizeLimit.value =
+        config.getEffectiveMaxObjectSizeLimit(projectState) == config
+            .getMaxObjectSizeLimit() ? config
+            .getFormattedMaxObjectSizeLimit() : p.getMaxObjectSizeLimit();
+    maxObjectSizeLimit.configuredValue = p.getMaxObjectSizeLimit();
+    maxObjectSizeLimit.inheritedValue =
+        config.getFormattedMaxObjectSizeLimit();
+    this.maxObjectSizeLimit = maxObjectSizeLimit;
+
+    this.submitType = p.getSubmitType();
+    this.state = p.getState() != Project.State.ACTIVE ? p.getState() : null;
+
+    this.commentlinks = Maps.newLinkedHashMap();
+    for (CommentLinkInfo cl : projectState.getCommentLinks()) {
+      this.commentlinks.put(cl.name, cl);
+    }
+
+    actions = Maps.newTreeMap();
+    for (UiAction.Description d : UiActions.from(
+        views, new ProjectResource(control),
+        Providers.of(control.getCurrentUser()))) {
+      actions.put(d.getId(), new ActionInfo(d));
+    }
+    this.theme = projectState.getTheme();
+  }
+
+  public static class InheritedBooleanInfo {
+    public Boolean value;
+    public InheritableBoolean configuredValue;
+    public Boolean inheritedValue;
+  }
+
+  public static class MaxObjectSizeLimitInfo {
+    public String value;
+    public String configuredValue;
+    public String inheritedValue;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
new file mode 100644
index 0000000..eab6ed9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -0,0 +1,238 @@
+// 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.project;
+
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.errors.InvalidRevisionException;
+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.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.CreateBranch.Input;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class CreateBranch implements RestModifyView<ProjectResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(CreateBranch.class);
+
+  static class Input {
+    String ref;
+
+    @DefaultInput
+    String revision;
+  }
+
+  static interface Factory {
+    CreateBranch create(String ref);
+  }
+
+  private final IdentifiedUser identifiedUser;
+  private final GitRepositoryManager repoManager;
+  private final GitReferenceUpdated referenceUpdated;
+  private final ChangeHooks hooks;
+  private String ref;
+
+  @Inject
+  CreateBranch(IdentifiedUser identifiedUser, GitRepositoryManager repoManager,
+      GitReferenceUpdated referenceUpdated, ChangeHooks hooks,
+      @Assisted String ref) {
+    this.identifiedUser = identifiedUser;
+    this.repoManager = repoManager;
+    this.referenceUpdated = referenceUpdated;
+    this.hooks = hooks;
+    this.ref = ref;
+  }
+
+  @Override
+  public BranchInfo apply(ProjectResource rsrc, Input input)
+      throws BadRequestException, AuthException, ResourceConflictException,
+      IOException {
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.ref != null && !ref.equals(input.ref)) {
+      throw new BadRequestException("ref must match URL");
+    }
+    if (input.revision == null) {
+      input.revision = Constants.HEAD;
+    }
+    while (ref.startsWith("/")) {
+      ref = ref.substring(1);
+    }
+    if (!ref.startsWith(Constants.R_REFS)) {
+      ref = Constants.R_HEADS + ref;
+    }
+    if (!Repository.isValidRefName(ref)) {
+      throw new BadRequestException("invalid branch name \"" + ref + "\"");
+    }
+    if (MagicBranch.isMagicBranch(ref)) {
+      throw new BadRequestException("not allowed to create branches under \""
+          + MagicBranch.getMagicRefNamePrefix(ref) + "\"");
+    }
+
+    final Branch.NameKey name = new Branch.NameKey(rsrc.getNameKey(), ref);
+    final RefControl refControl = rsrc.getControl().controlForRef(name);
+    final Repository repo = repoManager.openRepository(rsrc.getNameKey());
+    try {
+      final ObjectId revid = parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
+      final RevWalk rw = verifyConnected(repo, revid);
+      RevObject object = rw.parseAny(revid);
+
+      if (ref.startsWith(Constants.R_HEADS)) {
+        // Ensure that what we start the branch from is a commit. If we
+        // were given a tag, deference to the commit instead.
+        //
+        try {
+          object = rw.parseCommit(object);
+        } catch (IncorrectObjectTypeException notCommit) {
+          throw new BadRequestException("\"" + input.revision + "\" not a commit");
+        }
+      }
+
+      if (!refControl.canCreate(rw, object)) {
+        throw new AuthException("Cannot create \"" + ref + "\"");
+      }
+
+      try {
+        final RefUpdate u = repo.updateRef(ref);
+        u.setExpectedOldObjectId(ObjectId.zeroId());
+        u.setNewObjectId(object.copy());
+        u.setRefLogIdent(identifiedUser.newRefLogIdent());
+        u.setRefLogMessage("created via REST from " + input.revision, false);
+        final RefUpdate.Result result = u.update(rw);
+        switch (result) {
+          case FAST_FORWARD:
+          case NEW:
+          case NO_CHANGE:
+            referenceUpdated.fire(name.getParentKey(), u);
+            hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
+            break;
+          case LOCK_FAILURE:
+            if (repo.getRef(ref) != null) {
+              throw new ResourceConflictException("branch \"" + ref
+                  + "\" already exists");
+            }
+            String refPrefix = getRefPrefix(ref);
+            while (!Constants.R_HEADS.equals(refPrefix)) {
+              if (repo.getRef(refPrefix) != null) {
+                throw new ResourceConflictException("Cannot create branch \""
+                    + ref + "\" since it conflicts with branch \"" + refPrefix
+                    + "\".");
+              }
+              refPrefix = getRefPrefix(refPrefix);
+            }
+          default: {
+            throw new IOException(result.name());
+          }
+        }
+
+        BranchInfo b = new BranchInfo();
+        b.ref = ref;
+        b.revision = revid.getName();
+        b.setCanDelete(refControl.canDelete());
+        return b;
+      } catch (IOException err) {
+        log.error("Cannot create branch \"" + name + "\"", err);
+        throw err;
+      }
+    } catch (InvalidRevisionException e) {
+      throw new BadRequestException("invalid revision \"" + input.revision + "\"");
+    } finally {
+      repo.close();
+    }
+  }
+
+  private static String getRefPrefix(final String refName) {
+    final int i = refName.lastIndexOf('/');
+    if (i > Constants.R_HEADS.length() - 1) {
+      return refName.substring(0, i);
+    }
+    return Constants.R_HEADS;
+  }
+
+  private ObjectId parseBaseRevision(Repository repo,
+      Project.NameKey projectName, String baseRevision)
+      throws InvalidRevisionException {
+    try {
+      final ObjectId revid = repo.resolve(baseRevision);
+      if (revid == null) {
+        throw new InvalidRevisionException();
+      }
+      return revid;
+    } catch (IOException err) {
+      log.error("Cannot resolve \"" + baseRevision + "\" in project \""
+          + projectName.get() + "\"", err);
+      throw new InvalidRevisionException();
+    } catch (RevisionSyntaxException err) {
+      log.error("Invalid revision syntax \"" + baseRevision + "\"", err);
+      throw new InvalidRevisionException();
+    }
+  }
+
+  private RevWalk verifyConnected(final Repository repo, final ObjectId revid)
+      throws InvalidRevisionException {
+    try {
+      final ObjectWalk rw = new ObjectWalk(repo);
+      try {
+        rw.markStart(rw.parseCommit(revid));
+      } catch (IncorrectObjectTypeException err) {
+        throw new InvalidRevisionException();
+      }
+      for (final Ref r : repo.getRefDatabase().getRefs(ALL).values()) {
+        try {
+          rw.markUninteresting(rw.parseAny(r.getObjectId()));
+        } catch (MissingObjectException err) {
+          continue;
+        }
+      }
+      rw.checkConnectivity();
+      return rw;
+    } catch (IncorrectObjectTypeException err) {
+      throw new InvalidRevisionException();
+    } catch (MissingObjectException err) {
+      throw new InvalidRevisionException();
+    } catch (IOException err) {
+      log.error("Repository \"" + repo.getDirectory()
+          + "\" may be corrupt; suggest running git fsck", err);
+      throw new InvalidRevisionException();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 5494146..0d4f336 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -29,17 +29,21 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.project.CreateProject.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
-class CreateProject implements RestModifyView<TopLevelResource, Input> {
-  static class Input {
+public class CreateProject implements RestModifyView<TopLevelResource, Input> {
+  public static class Input {
     String name;
     String parent;
     String description;
@@ -52,9 +56,10 @@
     InheritableBoolean useSignedOffBy;
     InheritableBoolean useContentMerge;
     InheritableBoolean requireChangeId;
+    String maxObjectSizeLimit;
   }
 
-  static interface Factory {
+  public static interface Factory {
     CreateProject create(String name);
   }
 
@@ -79,7 +84,7 @@
   @Override
   public Object apply(TopLevelResource resource, Input input)
       throws BadRequestException, UnprocessableEntityException,
-      ProjectCreationFailedException {
+      ProjectCreationFailedException, IOException {
     if (input == null) {
       input = new Input();
     }
@@ -117,6 +122,12 @@
                 input.useContentMerge, InheritableBoolean.INHERIT);
     args.changeIdRequired =
         Objects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
+    try {
+      args.maxObjectSizeLimit =
+          ProjectConfig.validMaxObjectSizeLimit(input.maxObjectSizeLimit);
+    } catch (ConfigInvalidException e) {
+      throw new BadRequestException(e.getMessage());
+    }
 
     Project p = createProjectFactory.create(args).createProject();
     return Response.created(json.format(p));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 7bbd2e7..ea20cea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -35,6 +35,7 @@
   public InheritableBoolean contentMerge;
   public InheritableBoolean changeIdRequired;
   public boolean createEmptyCommit;
+  public String maxObjectSizeLimit;
 
   public CreateProjectArgs() {
     contributorAgreements = InheritableBoolean.INHERIT;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
new file mode 100644
index 0000000..a41c197
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.DeleteBranch.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class DeleteBranch implements RestModifyView<BranchResource, Input>{
+  private static final Logger log = LoggerFactory.getLogger(DeleteBranch.class);
+
+  static class Input {
+  }
+
+  private final IdentifiedUser identifiedUser;
+  private final GitRepositoryManager repoManager;
+  private final Provider<ReviewDb> dbProvider;
+  private final GitReferenceUpdated referenceUpdated;
+  private final ChangeHooks hooks;
+
+  @Inject
+  DeleteBranch(IdentifiedUser identifiedUser, GitRepositoryManager repoManager,
+      Provider<ReviewDb> dbProvider, GitReferenceUpdated referenceUpdated,
+      ChangeHooks hooks) {
+    this.identifiedUser = identifiedUser;
+    this.repoManager = repoManager;
+    this.dbProvider = dbProvider;
+    this.referenceUpdated = referenceUpdated;
+    this.hooks = hooks;
+  }
+
+  @Override
+  public Object apply(BranchResource rsrc, Input input) throws AuthException,
+      ResourceConflictException, OrmException, IOException {
+    if (!rsrc.getControl().controlForRef(rsrc.getBranchKey()).canDelete()) {
+      throw new AuthException("Cannot delete branch");
+    }
+    if (dbProvider.get().changes().byBranchOpenAll(rsrc.getBranchKey())
+        .iterator().hasNext()) {
+      throw new ResourceConflictException("branch " + rsrc.getBranchKey()
+          + " has open changes");
+    }
+
+    Repository r = repoManager.openRepository(rsrc.getNameKey());
+    try {
+      RefUpdate.Result result;
+      RefUpdate u;
+      try {
+        u = r.updateRef(rsrc.getRef());
+        u.setForceUpdate(true);
+        result = u.delete();
+      } catch (IOException e) {
+        log.error("Cannot delete " + rsrc.getBranchKey(), e);
+        throw e;
+      }
+
+      switch (result) {
+        case NEW:
+        case NO_CHANGE:
+        case FAST_FORWARD:
+        case FORCED:
+          referenceUpdated.fire(rsrc.getNameKey(), u);
+          hooks.doRefUpdatedHook(rsrc.getBranchKey(), u, identifiedUser.getAccount());
+          break;
+
+        case REJECTED_CURRENT_BRANCH:
+          log.warn("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
+          throw new ResourceConflictException("cannot delete current branch");
+
+        default:
+          log.error("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
+          throw new ResourceConflictException("cannot delete branch: " + result.name());
+      }
+    } finally {
+      r.close();
+    }
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
index d4d923e..ca7f6f0 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
@@ -18,8 +18,8 @@
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.StreamingResponse;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.project.GarbageCollect.Input;
 import com.google.inject.Inject;
@@ -43,15 +43,10 @@
   }
 
   @Override
-  public StreamingResponse apply(final ProjectResource rsrc, Input input) {
-    return new StreamingResponse() {
+  public BinaryResult apply(final ProjectResource rsrc, Input input) {
+    return new BinaryResult() {
       @Override
-      public String getContentType() {
-        return "text/plain;charset=UTF-8";
-      }
-
-      @Override
-      public void stream(OutputStream out) throws IOException {
+      public void writeTo(OutputStream out) throws IOException {
         PrintWriter writer = new PrintWriter(
             new OutputStreamWriter(out, Charsets.UTF_8)) {
           @Override
@@ -88,6 +83,8 @@
           writer.flush();
         }
       }
-    };
+    }.setContentType("text/plain")
+     .setCharacterEncoding(Charsets.UTF_8.name())
+     .disableGzip();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
new file mode 100644
index 0000000..781cf01
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
@@ -0,0 +1,26 @@
+// 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.project;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+
+public class GetBranch implements RestReadView<BranchResource> {
+
+  @Override
+  public BranchInfo apply(BranchResource rsrc) {
+    return rsrc.getBranchInfo();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
new file mode 100644
index 0000000..1659cb7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
@@ -0,0 +1,43 @@
+// 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.project;
+
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+public class GetChildProject implements RestReadView<ChildProjectResource> {
+  @Option(name = "--recursive", usage = "to list child projects recursively")
+  private boolean recursive;
+
+  private final ProjectJson json;
+
+  @Inject
+  GetChildProject(ProjectJson json) {
+    this.json = json;
+  }
+
+  @Override
+  public ProjectInfo apply(ChildProjectResource rsrc)
+      throws ResourceNotFoundException {
+    if (recursive || rsrc.isDirectChild()) {
+      return json.format(rsrc.getChild().getProject());
+    }
+    throw new ResourceNotFoundException(rsrc.getName());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index cd5e5d7..6f78651 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -14,81 +14,29 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.server.git.GitRepositoryManager;
-
-import java.util.Map;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 public class GetConfig implements RestReadView<ProjectResource> {
 
+  private final TransferConfig config;
+  private final DynamicMap<RestView<ProjectResource>> views;
+
+  @Inject
+  public GetConfig(TransferConfig config,
+      DynamicMap<RestView<ProjectResource>> views,
+      Provider<CurrentUser> currentUser) {
+    this.config = config;
+    this.views = views;
+  }
+
   @Override
   public ConfigInfo apply(ProjectResource resource) {
-    ConfigInfo result = new ConfigInfo();
-    RefControl refConfig = resource.getControl()
-        .controlForRef(GitRepositoryManager.REF_CONFIG);
-    ProjectState state = resource.getControl().getProjectState();
-    if (refConfig.isVisible()) {
-      InheritedBooleanInfo useContributorAgreements = new InheritedBooleanInfo();
-      InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
-      InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
-      InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
-
-      useContributorAgreements.value = state.isUseContributorAgreements();
-      useSignedOffBy.value = state.isUseSignedOffBy();
-      useContentMerge.value = state.isUseContentMerge();
-      requireChangeId.value = state.isRequireChangeID();
-
-      Project p = state.getProject();
-      useContributorAgreements.configuredValue = p.getUseContributorAgreements();
-      useSignedOffBy.configuredValue = p.getUseSignedOffBy();
-      useContentMerge.configuredValue = p.getUseContentMerge();
-      requireChangeId.configuredValue = p.getRequireChangeID();
-
-      ProjectState parentState = Iterables.getFirst(state.parents(), null);
-      if (parentState != null) {
-        useContributorAgreements.inheritedValue = parentState.isUseContributorAgreements();
-        useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
-        useContentMerge.inheritedValue = parentState.isUseContentMerge();
-        requireChangeId.inheritedValue = parentState.isRequireChangeID();
-      }
-
-      result.useContributorAgreements = useContributorAgreements;
-      result.useSignedOffBy = useSignedOffBy;
-      result.useContentMerge = useContentMerge;
-      result.requireChangeId = requireChangeId;
-    }
-
-    // commentlinks are visible to anyone, as they are used for linkification
-    // on the client side.
-    result.commentlinks = Maps.newLinkedHashMap();
-    for (CommentLinkInfo cl : state.getCommentLinks()) {
-      result.commentlinks.put(cl.name, cl);
-    }
-
-    // Themes are visible to anyone, as they are rendered client-side.
-    result.theme = state.getTheme();
-    return result;
-  }
-
-  public static class ConfigInfo {
-    public final String kind = "gerritcodereview#project_config";
-
-    public InheritedBooleanInfo useContributorAgreements;
-    public InheritedBooleanInfo useContentMerge;
-    public InheritedBooleanInfo useSignedOffBy;
-    public InheritedBooleanInfo requireChangeId;
-
-    public Map<String, CommentLinkInfo> commentlinks;
-    public ThemeInfo theme;
-  }
-
-  public static class InheritedBooleanInfo {
-    public Boolean value;
-    public InheritableBoolean configuredValue;
-    public Boolean inheritedValue;
+    return new ConfigInfo(resource.getControl(), config, views);
   }
 }
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
new file mode 100644
index 0000000..7ed5b5f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -0,0 +1,161 @@
+// 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.project;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ListBranches implements RestReadView<ProjectResource> {
+
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  public ListBranches(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public List<BranchInfo> apply(ProjectResource rsrc)
+      throws ResourceNotFoundException, IOException {
+    List<BranchInfo> branches = Lists.newArrayList();
+
+    BranchInfo headBranch = null;
+    BranchInfo configBranch = null;
+    final Set<String> targets = Sets.newHashSet();
+
+    final Repository db;
+    try {
+      db = repoManager.openRepository(rsrc.getNameKey());
+    } catch (RepositoryNotFoundException noGitRepository) {
+      throw new ResourceNotFoundException();
+    }
+
+    try {
+      final Map<String, Ref> all = db.getRefDatabase().getRefs(RefDatabase.ALL);
+
+      if (!all.containsKey(Constants.HEAD)) {
+        // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
+        // filtered it out. If we ask for it individually we can find the
+        // underlying target and put it into the map anyway.
+        //
+        try {
+          Ref head = db.getRef(Constants.HEAD);
+          if (head != null) {
+            all.put(Constants.HEAD, head);
+          }
+        } catch (IOException e) {
+          // Ignore the failure reading HEAD.
+        }
+      }
+
+      for (final Ref ref : all.values()) {
+        if (ref.isSymbolic()) {
+          targets.add(ref.getTarget().getName());
+        }
+      }
+
+      for (final Ref ref : all.values()) {
+        if (ref.isSymbolic()) {
+          // A symbolic reference to another branch, instead of
+          // showing the resolved value, show the name it references.
+          //
+          String target = ref.getTarget().getName();
+          RefControl targetRefControl = rsrc.getControl().controlForRef(target);
+          if (!targetRefControl.isVisible()) {
+            continue;
+          }
+          if (target.startsWith(Constants.R_HEADS)) {
+            target = target.substring(Constants.R_HEADS.length());
+          }
+
+          BranchInfo b = new BranchInfo();
+          b.ref = ref.getName();
+          b.revision = target;
+
+          if (Constants.HEAD.equals(ref.getName())) {
+            b.setCanDelete(false);
+            headBranch = b;
+          } else {
+            b.setCanDelete(targetRefControl.canDelete());
+            branches.add(b);
+          }
+          continue;
+        }
+
+        final RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
+        if (refControl.isVisible()) {
+          if (ref.getName().startsWith(Constants.R_HEADS)) {
+            branches.add(createBranchInfo(ref, refControl, targets));
+          } else if (GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
+            configBranch = createBranchInfo(ref, refControl, targets);
+          }
+        }
+      }
+    } finally {
+      db.close();
+    }
+    Collections.sort(branches, new Comparator<BranchInfo>() {
+      @Override
+      public int compare(final BranchInfo a, final BranchInfo b) {
+        return a.ref.compareTo(b.ref);
+      }
+    });
+    if (configBranch != null) {
+      branches.add(0, configBranch);
+    }
+    if (headBranch != null) {
+      branches.add(0, headBranch);
+    }
+    return branches;
+  }
+
+  private static BranchInfo createBranchInfo(Ref ref, RefControl refControl,
+      Set<String> targets) {
+    BranchInfo b = new BranchInfo();
+    b.ref = ref.getName();
+    if (ref.getObjectId() != null) {
+      b.revision = ref.getObjectId().name();
+    }
+    b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
+    return b;
+  }
+
+  public static class BranchInfo {
+    public String ref;
+    public String revision;
+    public Boolean canDelete;
+
+    void setCanDelete(boolean canDelete) {
+      this.canDelete = canDelete ? true : null;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
new file mode 100644
index 0000000..d4b84dd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -0,0 +1,106 @@
+// 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.project;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.List;
+import java.util.Map;
+
+public class ListChildProjects implements RestReadView<ProjectResource> {
+
+  @Option(name = "--recursive", usage = "to list child projects recursively")
+  private boolean recursive;
+
+  private final ProjectCache projectCache;
+  private final AllProjectsName allProjects;
+  private final ProjectJson json;
+  private final ProjectNode.Factory projectNodeFactory;
+
+  @Inject
+  ListChildProjects(ProjectCache projectCache, AllProjectsName allProjects,
+      ProjectJson json, ProjectNode.Factory projectNodeFactory) {
+    this.projectCache = projectCache;
+    this.allProjects = allProjects;
+    this.json = json;
+    this.projectNodeFactory = projectNodeFactory;
+  }
+
+  @Override
+  public List<ProjectInfo> apply(ProjectResource rsrc) {
+    if (recursive) {
+      return getChildProjectsRecursively(rsrc.getNameKey(),
+          rsrc.getControl().getCurrentUser());
+    } else {
+      return getDirectChildProjects(rsrc.getNameKey());
+    }
+  }
+
+  private List<ProjectInfo> getDirectChildProjects(Project.NameKey parent) {
+    List<ProjectInfo> childProjects = Lists.newArrayList();
+    for (Project.NameKey projectName : projectCache.all()) {
+      ProjectState e = projectCache.get(projectName);
+      if (e == null) {
+        // If we can't get it from the cache, pretend it's not present.
+        continue;
+      }
+      if (parent.equals(e.getProject().getParent(allProjects))) {
+        childProjects.add(json.format(e.getProject()));
+      }
+    }
+    return childProjects;
+  }
+
+  private List<ProjectInfo> getChildProjectsRecursively(Project.NameKey parent,
+      CurrentUser user) {
+    Map<Project.NameKey, ProjectNode> projects = Maps.newHashMap();
+    for (Project.NameKey name : projectCache.all()) {
+      ProjectState p = projectCache.get(name);
+      if (p == null) {
+        // If we can't get it from the cache, pretend it's not present.
+        continue;
+      }
+      projects.put(name, projectNodeFactory.create(p.getProject(),
+          p.controlFor(user).isVisible()));
+    }
+    for (ProjectNode key : projects.values()) {
+      ProjectNode node = projects.get(key.getParentName());
+      if (node != null) {
+        node.addChild(key);
+      }
+    }
+    return getChildProjectsRecursively(projects.get(parent));
+  }
+
+  private List<ProjectInfo> getChildProjectsRecursively(ProjectNode p) {
+    List<ProjectInfo> allChildren = Lists.newArrayList();
+    for (ProjectNode c : p.getChildren()) {
+      if (c.isVisible()) {
+        allChildren.add(json.format(c.getProject()));
+        allChildren.addAll(getChildProjectsRecursively(c));
+      }
+    }
+    return allChildren;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 1c61d96..0ef875e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.gerrit.server.project.BranchResource.BRANCH_KIND;
+import static com.google.gerrit.server.project.ChildProjectResource.CHILD_PROJECT_KIND;
 import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.project.CreateProject;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class Module extends RestApiModule {
@@ -29,6 +30,8 @@
     bind(DashboardsCollection.class);
 
     DynamicMap.mapOf(binder(), PROJECT_KIND);
+    DynamicMap.mapOf(binder(), CHILD_PROJECT_KIND);
+    DynamicMap.mapOf(binder(), BRANCH_KIND);
     DynamicMap.mapOf(binder(), DASHBOARD_KIND);
 
     put(PROJECT_KIND).to(PutProject.class);
@@ -40,12 +43,21 @@
     get(PROJECT_KIND, "parent").to(GetParent.class);
     put(PROJECT_KIND, "parent").to(SetParent.class);
 
+    child(PROJECT_KIND, "children").to(ChildProjectsCollection.class);
+    get(CHILD_PROJECT_KIND).to(GetChildProject.class);
+
     get(PROJECT_KIND, "HEAD").to(GetHead.class);
     put(PROJECT_KIND, "HEAD").to(SetHead.class);
 
     get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
     post(PROJECT_KIND, "gc").to(GarbageCollect.class);
 
+    child(PROJECT_KIND, "branches").to(BranchesCollection.class);
+    put(BRANCH_KIND).to(PutBranch.class);
+    get(BRANCH_KIND).to(GetBranch.class);
+    delete(BRANCH_KIND).to(DeleteBranch.class);
+    install(new FactoryModuleBuilder().build(CreateBranch.Factory.class));
+
     child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
     get(DASHBOARD_KIND).to(GetDashboard.class);
     put(DASHBOARD_KIND).to(SetDashboard.class);
@@ -53,5 +65,6 @@
     install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
 
     get(PROJECT_KIND, "config").to(GetConfig.class);
+    put(PROJECT_KIND, "config").to(PutConfig.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
index d68725f..86410af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
@@ -118,7 +118,11 @@
           }
         };
         for (NewProjectCreatedListener l : createdListener) {
-          l.onNewProjectCreated(event);
+          try {
+            l.onNewProjectCreated(event);
+          } catch (RuntimeException e) {
+            log.warn("Failure in NewProjectCreatedListener", e);
+          }
         }
 
         final RefUpdate u = repo.updateRef(Constants.HEAD);
@@ -181,6 +185,7 @@
       newProject.setUseSignedOffBy(createProjectArgs.signedOffBy);
       newProject.setUseContentMerge(createProjectArgs.contentMerge);
       newProject.setRequireChangeID(createProjectArgs.changeIdRequired);
+      newProject.setMaxObjectSizeLimit(createProjectArgs.maxObjectSizeLimit);
       if (createProjectArgs.newParent != null) {
         newProject.setParentName(createProjectArgs.newParent.getProject()
             .getNameKey());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index 483ecaf..6647652 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -77,7 +77,7 @@
 
       boolean perUser = false;
       Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap();
-      for (SectionMatcher matcher : matcherList) {
+      for (SectionMatcher sm : matcherList) {
         // If the matcher has to expand parameters and its prefix matches the
         // reference there is a very good chance the reference is actually user
         // specific, even if the matcher does not match the reference. Since its
@@ -91,12 +91,12 @@
         // references are usually less frequent than the non-user references.
         //
         if (username != null && !perUser
-            && matcher instanceof SectionMatcher.ExpandParameters) {
-          perUser = ((SectionMatcher.ExpandParameters) matcher).matchPrefix(ref);
+            && sm.matcher instanceof RefPatternMatcher.ExpandParameters) {
+          perUser = ((RefPatternMatcher.ExpandParameters) sm.matcher).matchPrefix(ref);
         }
 
-        if (matcher.match(ref, username)) {
-          sectionToProject.put(matcher.section, matcher.project);
+        if (sm.match(ref, username)) {
+          sectionToProject.put(sm.section, sm.project);
         }
       }
       List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 697b838..0e5cecb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 
+import java.io.IOException;
 import java.util.Set;
 
 /** Cache of project information, including access rights. */
@@ -28,13 +29,27 @@
    * Get the cached data for a project by its unique name.
    *
    * @param projectName name of the project.
-   * @return the cached data; null if no such project exists.
+   * @return the cached data; null if no such project exists or a error occured.
+   * @see #checkedGet(com.google.gerrit.reviewdb.client.Project.NameKey)
    */
   public ProjectState get(Project.NameKey projectName);
 
+  /**
+   * Get the cached data for a project by its unique name.
+   *
+   * @param projectName name of the project.
+   * @throws IOException when there was an error.
+   * @return the cached data; null if no such project exists.
+   */
+  public ProjectState checkedGet(Project.NameKey projectName)
+      throws IOException;
+
   /** Invalidate the cached information about the given project. */
   public void evict(Project p);
 
+  /** Invalidate the cached information about the given project. */
+  public void evict(Project.NameKey p);
+
   /**
    * Remove information about the given project from the cache. It will no
    * longer be returned from {@link #all()}.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 272c128..8ccbca3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Sets;
@@ -34,6 +35,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
@@ -101,13 +103,18 @@
     return state;
   }
 
-  /**
-   * Get the cached data for a project by its unique name.
-   *
-   * @param projectName name of the project.
-   * @return the cached data; null if no such project exists.
-   */
+  @Override
   public ProjectState get(final Project.NameKey projectName) {
+     try {
+      return checkedGet(projectName);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override
+  public ProjectState checkedGet(Project.NameKey projectName)
+      throws IOException {
     if (projectName == null) {
       return null;
     }
@@ -121,18 +128,27 @@
     } catch (ExecutionException e) {
       if (!(e.getCause() instanceof RepositoryNotFoundException)) {
         log.warn(String.format("Cannot read project %s", projectName.get()), e);
+        Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
+        throw new IOException(e);
       }
       return null;
     }
   }
 
-  /** Invalidate the cached information about the given project. */
+  @Override
   public void evict(final Project p) {
     if (p != null) {
       byName.invalidate(p.getNameKey().get());
     }
   }
 
+  /** Invalidate the cached information about the given project. */
+  public void evict(final Project.NameKey p) {
+    if (p != null) {
+      byName.invalidate(p.get());
+    }
+  }
+
   @Override
   public void remove(final Project p) {
     listLock.lock();
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 85637e0..ddfe752 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
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.server.project;
 
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Capable;
@@ -28,7 +31,6 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
@@ -58,8 +60,6 @@
 import java.util.Set;
 import java.util.Map.Entry;
 
-import javax.annotation.Nullable;
-
 /** Access control management for a user accessing a project's data. */
 public class ProjectControl {
   public static final int VISIBLE = 1 << 0;
@@ -76,13 +76,25 @@
     }
 
     public ProjectControl controlFor(Project.NameKey nameKey, CurrentUser user)
-        throws NoSuchProjectException {
-      final ProjectState p = projectCache.get(nameKey);
+        throws NoSuchProjectException, IOException {
+      final ProjectState p = projectCache.checkedGet(nameKey);
       if (p == null) {
         throw new NoSuchProjectException(nameKey);
       }
       return p.controlFor(user);
     }
+
+    public ProjectControl validateFor(Project.NameKey nameKey, int need,
+        CurrentUser user) throws NoSuchProjectException, IOException {
+      final ProjectControl c = controlFor(nameKey, user);
+      if ((need & VISIBLE) == VISIBLE && c.isVisible()) {
+        return c;
+      }
+      if ((need & OWNER) == OWNER && c.isOwner()) {
+        return c;
+      }
+      throw new NoSuchProjectException(nameKey);
+    }
   }
 
   public static class Factory {
@@ -221,6 +233,20 @@
         || isOwnerAnyRef());
   }
 
+  public boolean canUpload() {
+    for (SectionMatcher matcher : access()) {
+      AccessSection section = matcher.section;
+      if (section.getName().startsWith("refs/for/")) {
+        Permission permission = section.getPermission(Permission.PUSH);
+        if (permission != null
+            && controlForRef(section.getName()).canPerform(Permission.PUSH)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   /** Can this user see all the refs in this projects? */
   public boolean allRefsAreVisible() {
     return allRefsAreVisibleExcept(Collections.<String> emptySet());
@@ -286,7 +312,7 @@
   }
 
   private Capable verifyActiveContributorAgreement() {
-    if (! (user instanceof IdentifiedUser)) {
+    if (! (user.isIdentifiedUser())) {
       return new Capable("Must be logged in to verify Contributor Agreement");
     }
     final IdentifiedUser iUser = (IdentifiedUser) user;
@@ -464,11 +490,16 @@
   }
 
   public boolean canReadCommit(RevWalk rw, RevCommit commit) {
-    NameKey projName = state.getProject().getNameKey();
+    if (controlForRef("refs/*").canPerform(Permission.READ)) {
+      return true;
+    }
+
+    Project.NameKey projName = state.getProject().getNameKey();
     try {
       Repository repo = repoManager.openRepository(projName);
       try {
-        for (Entry<String, Ref> entry : repo.getAllRefs().entrySet()) {
+        Map<String, Ref> allRefs = repo.getRefDatabase().getRefs(ALL);
+        for (Entry<String, Ref> entry : allRefs.entrySet()) {
           String refName = entry.getKey();
           if (!refName.startsWith("refs/heads") && !refName.startsWith("refs/tags")) {
             continue;
@@ -494,6 +525,6 @@
               commit.name(), projName.get());
       log.error(msg, e);
     }
-    return controlForRef("refs/*").canPerform(Permission.READ);
+    return false;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
index 1c4d7c4..5b4b334 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
@@ -57,6 +57,10 @@
     return allProjectsName.equals(project.getNameKey());
   }
 
+  public Project getProject() {
+    return project;
+  }
+
   @Override
   public String getDisplayName() {
     return project.getName();
@@ -68,7 +72,7 @@
   }
 
   @Override
-  public SortedSet<? extends TreeNode> getChildren() {
+  public SortedSet<? extends ProjectNode> getChildren() {
     return children;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
index aeca0e8..f4449f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
@@ -25,10 +25,14 @@
 
   private final ProjectControl control;
 
-  ProjectResource(ProjectControl control) {
+  public ProjectResource(ProjectControl control) {
     this.control = control;
   }
 
+  ProjectResource(ProjectResource rsrc) {
+    this.control = rsrc.getControl();
+  }
+
   public String getName() {
     return control.getProject().getName();
   }
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 6f80841..800fa6e 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
@@ -216,6 +216,10 @@
     return config;
   }
 
+  public long getMaxObjectSizeLimit() {
+    return config.getMaxObjectSizeLimit();
+  }
+
   /** Get the sections that pertain only to this project. */
   List<SectionMatcher> getLocalAccessSections() {
     List<SectionMatcher> sm = localAccessSections;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
index 4c00439..6e9c5d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -28,6 +28,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import java.io.IOException;
+
 public class ProjectsCollection implements
     RestCollection<TopLevelResource, ProjectResource>,
     AcceptsCreate<TopLevelResource> {
@@ -56,7 +58,7 @@
 
   @Override
   public ProjectResource parse(TopLevelResource parent, IdString id)
-      throws ResourceNotFoundException {
+      throws ResourceNotFoundException, IOException {
     ProjectResource rsrc = _parse(id.get());
     if (rsrc == null) {
       throw new ResourceNotFoundException(id);
@@ -71,8 +73,10 @@
    * @return the project
    * @throws UnprocessableEntityException thrown if the project ID cannot be
    *         resolved or if the project is not visible to the calling user
+   * @throws IOException thrown when there is an error.
    */
-  public ProjectResource parse(String id) throws UnprocessableEntityException {
+  public ProjectResource parse(String id)
+      throws UnprocessableEntityException, IOException {
     ProjectResource rsrc = _parse(id);
     if (rsrc == null) {
       throw new UnprocessableEntityException(String.format(
@@ -81,7 +85,7 @@
     return rsrc;
   }
 
-  private ProjectResource _parse(String id) {
+  private ProjectResource _parse(String id) throws IOException {
     ProjectControl ctl;
     try {
       ctl = controlFactory.controlFor(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
new file mode 100644
index 0000000..2cd5659
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
@@ -0,0 +1,29 @@
+// 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.project;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.CreateBranch.Input;
+
+public class PutBranch implements RestModifyView<BranchResource, Input> {
+
+  @Override
+  public Object apply(BranchResource rsrc, Input input)
+      throws ResourceConflictException {
+    throw new ResourceConflictException("Branch \"" + rsrc.getBranchInfo().ref
+        + "\" already exists");
+  }
+}
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
new file mode 100644
index 0000000..cb25af3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -0,0 +1,155 @@
+// 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.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.project.PutConfig.Input;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
+public class PutConfig implements RestModifyView<ProjectResource, Input> {
+  public static class Input {
+    public String description;
+    public InheritableBoolean useContributorAgreements;
+    public InheritableBoolean useContentMerge;
+    public InheritableBoolean useSignedOffBy;
+    public InheritableBoolean requireChangeId;
+    public String maxObjectSizeLimit;
+    public SubmitType submitType;
+    public Project.State state;
+  }
+
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final ProjectCache projectCache;
+  private final Provider<CurrentUser> self;
+  private final ProjectState.Factory projectStateFactory;
+  private final TransferConfig config;
+  private final DynamicMap<RestView<ProjectResource>> views;
+  private final Provider<CurrentUser> currentUser;
+
+  @Inject
+  PutConfig(MetaDataUpdate.User metaDataUpdateFactory,
+      ProjectCache projectCache,
+      Provider<CurrentUser> self,
+      ProjectState.Factory projectStateFactory,
+      TransferConfig config,
+      DynamicMap<RestView<ProjectResource>> views,
+      Provider<CurrentUser> currentUser) {
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.projectCache = projectCache;
+    this.self = self;
+    this.projectStateFactory = projectStateFactory;
+    this.config = config;
+    this.views = views;
+    this.currentUser = currentUser;
+  }
+
+  @Override
+  public ConfigInfo apply(ProjectResource rsrc, Input input)
+      throws ResourceNotFoundException, BadRequestException,
+      ResourceConflictException {
+    Project.NameKey projectName = rsrc.getNameKey();
+    if (!rsrc.getControl().isOwner()) {
+      throw new ResourceNotFoundException(projectName.get());
+    }
+
+    if (input == null) {
+      throw new BadRequestException("config is required");
+    }
+
+    final MetaDataUpdate md;
+    try {
+      md = metaDataUpdateFactory.create(projectName);
+    } catch (RepositoryNotFoundException notFound) {
+      throw new ResourceNotFoundException(projectName.get());
+    } catch (IOException e) {
+      throw new ResourceNotFoundException(projectName.get(), e);
+    }
+    try {
+      ProjectConfig projectConfig = ProjectConfig.read(md);
+      Project p = projectConfig.getProject();
+
+      p.setDescription(Strings.emptyToNull(input.description));
+
+      if (input.useContributorAgreements != null) {
+        p.setUseContributorAgreements(input.useContributorAgreements);
+      }
+      if (input.useContentMerge != null) {
+        p.setUseContentMerge(input.useContentMerge);
+      }
+      if (input.useSignedOffBy != null) {
+        p.setUseSignedOffBy(input.useSignedOffBy);
+      }
+      if (input.requireChangeId != null) {
+        p.setRequireChangeID(input.requireChangeId);
+      }
+
+      if (input.maxObjectSizeLimit != null) {
+        p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
+      }
+
+      if (input.submitType != null) {
+        p.setSubmitType(input.submitType);
+      }
+
+      if (input.state != null) {
+        p.setState(input.state);
+      }
+
+      md.setMessage("Modified project settings\n");
+      try {
+        projectConfig.commit(md);
+        (new PerRequestProjectControlCache(projectCache, self.get()))
+            .evict(projectConfig.getProject());
+      } catch (IOException e) {
+        if (e.getCause() instanceof ConfigInvalidException) {
+          throw new ResourceConflictException("Cannot update " + projectName
+              + ": " + e.getCause().getMessage());
+        } else {
+          throw new ResourceConflictException("Cannot update " + projectName);
+        }
+      }
+
+      ProjectState state = projectStateFactory.create(projectConfig);
+      return new ConfigInfo(
+          state.controlFor(currentUser.get()),
+          config, views);
+    } catch (ConfigInvalidException err) {
+      throw new ResourceConflictException("Cannot read project " + projectName, err);
+    } catch (IOException err) {
+      throw new ResourceConflictException("Cannot update project " + projectName, err);
+    } finally {
+      md.close();
+    }
+  }
+}
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 7c465cd..849d6f2 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
@@ -263,7 +263,7 @@
       final PersonIdent tagger = tag.getTaggerIdent();
       if (tagger != null) {
         boolean valid;
-        if (getCurrentUser() instanceof IdentifiedUser) {
+        if (getCurrentUser().isIdentifiedUser()) {
           final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
           final String addr = tagger.getEmailAddress();
           valid = user.getEmailAddresses().contains(addr);
@@ -307,7 +307,10 @@
     switch (getCurrentUser().getAccessPath()) {
       case REST_API:
       case JSON_RPC:
-        return isOwner() || canPushWithForce();
+      case SSH_COMMAND:
+        return getCurrentUser().getCapabilities().canAdministrateServer()
+            || (isOwner() && !isForceBlocked(Permission.PUSH))
+            || canPushWithForce();
 
       case GIT:
         return canPushWithForce();
@@ -396,7 +399,7 @@
 
   /** The range of permitted values associated with a label permission. */
   public PermissionRange getRange(String permission) {
-    if (Permission.isLabel(permission)) {
+    if (Permission.hasRange(permission)) {
       return toRange(permission, access(permission));
     }
     return null;
@@ -495,6 +498,22 @@
     return blocks.isEmpty() && !allows.isEmpty();
   }
 
+  /** True if for this permission force is blocked for the user. Works only for non labels. */
+  private boolean isForceBlocked(String permissionName) {
+    List<PermissionRule> access = access(permissionName);
+    Set<ProjectRef> allows = Sets.newHashSet();
+    Set<ProjectRef> blocks = Sets.newHashSet();
+    for (PermissionRule rule : access) {
+      if (rule.isBlock()) {
+        blocks.add(relevant.getRuleProps(rule));
+      } else if (rule.getForce()) {
+        allows.add(relevant.getRuleProps(rule));
+      }
+    }
+    blocks.removeAll(allows);
+    return !blocks.isEmpty();
+  }
+
   /** Rules for the given permission, or the empty list. */
   private List<PermissionRule> access(String permissionName) {
     List<PermissionRule> rules = effective.get(permissionName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
new file mode 100644
index 0000000..b71d194
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
@@ -0,0 +1,122 @@
+// 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.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+import com.google.gerrit.common.data.ParameterizedString;
+import dk.brics.automaton.Automaton;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+abstract class RefPatternMatcher {
+  public static RefPatternMatcher getMatcher(String pattern) {
+    if (pattern.contains("${")) {
+      return new ExpandParameters(pattern);
+    } else if (isRE(pattern)) {
+      return new Regexp(pattern);
+    } else if (pattern.endsWith("/*")) {
+      return new Prefix(pattern.substring(0, pattern.length() - 1));
+    } else {
+      return new Exact(pattern);
+    }
+  }
+
+  abstract boolean match(String ref, String username);
+
+  private static class Exact extends RefPatternMatcher {
+    private final String expect;
+
+    Exact(String name) {
+      expect = name;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return expect.equals(ref);
+    }
+  }
+
+  private static class Prefix extends RefPatternMatcher {
+    private final String prefix;
+
+    Prefix(String pfx) {
+      prefix = pfx;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return ref.startsWith(prefix);
+    }
+  }
+
+  private static class Regexp extends RefPatternMatcher {
+    private final Pattern pattern;
+
+    Regexp(String re) {
+      pattern = Pattern.compile(re);
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return pattern.matcher(ref).matches();
+    }
+  }
+
+  static class ExpandParameters extends RefPatternMatcher {
+    private final ParameterizedString template;
+    private final String prefix;
+
+    ExpandParameters(String pattern) {
+      template = new ParameterizedString(pattern);
+
+      if (isRE(pattern)) {
+        // Replace ${username} with ":USERNAME:" as : is not legal
+        // in a reference and the string :USERNAME: is not likely to
+        // be a valid part of the regex. This later allows the pattern
+        // prefix to be clipped, saving time on evaluation.
+        Automaton am =
+            RefControl.toRegExp(
+                template.replace(Collections.singletonMap("username",
+                    ":USERNAME:"))).toAutomaton();
+        String rePrefix = am.getCommonPrefix();
+        prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
+      } else {
+        prefix = pattern.substring(0, pattern.indexOf("${"));
+      }
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      if (!ref.startsWith(prefix) || username == null) {
+        return false;
+      }
+
+      String u;
+      if (isRE(template.getPattern())) {
+        u = username.replace(".", "\\.");
+      } else {
+        u = username;
+      }
+
+      RefPatternMatcher next =
+          getMatcher(template.replace(Collections.singletonMap("username", u)));
+      return next != null ? next.match(ref, username) : false;
+    }
+
+    boolean matchPrefix(String ref) {
+      return ref.startsWith(prefix);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
index 6f8af80..44c8b9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -14,147 +14,38 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.server.project.RefControl.isRE;
-
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.Project;
 
-import dk.brics.automaton.Automaton;
-
-import java.util.Collections;
-import java.util.regex.Pattern;
-
 /**
  * Matches an AccessSection against a reference name.
  * <p>
  * These matchers are "compiled" versions of the AccessSection name, supporting
  * faster selection of which sections are relevant to any given input reference.
  */
-abstract class SectionMatcher {
+class SectionMatcher extends RefPatternMatcher {
   static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
     String ref = section.getName();
     if (AccessSection.isValid(ref)) {
-      return wrap(project, ref, section);
+      return new SectionMatcher(project, section, getMatcher(ref));
     } else {
       return null;
     }
   }
 
-  static SectionMatcher wrap(Project.NameKey project, String pattern,
-      AccessSection section) {
-    if (pattern.contains("${")) {
-      return new ExpandParameters(project, pattern, section);
-
-    } else if (isRE(pattern)) {
-      return new Regexp(project, pattern, section);
-
-    } else if (pattern.endsWith("/*")) {
-      return new Prefix(project, pattern.substring(0, pattern.length() - 1),
-          section);
-
-    } else {
-      return new Exact(project, pattern, section);
-    }
-  }
-
   final Project.NameKey project;
   final AccessSection section;
+  final RefPatternMatcher matcher;
 
-  SectionMatcher(Project.NameKey project, AccessSection section) {
+  SectionMatcher(Project.NameKey project, AccessSection section,
+      RefPatternMatcher matcher) {
     this.project = project;
     this.section = section;
+    this.matcher = matcher;
   }
 
-  abstract boolean match(String ref, String username);
-
-  private static class Exact extends SectionMatcher {
-    private final String expect;
-
-    Exact(Project.NameKey project, String name, AccessSection section) {
-      super(project, section);
-      expect = name;
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      return expect.equals(ref);
-    }
-  }
-
-  private static class Prefix extends SectionMatcher {
-    private final String prefix;
-
-    Prefix(Project.NameKey project, String pfx, AccessSection section) {
-      super(project, section);
-      prefix = pfx;
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      return ref.startsWith(prefix);
-    }
-  }
-
-  private static class Regexp extends SectionMatcher {
-    private final Pattern pattern;
-
-    Regexp(Project.NameKey project, String re, AccessSection section) {
-      super(project, section);
-      pattern = Pattern.compile(re);
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      return pattern.matcher(ref).matches();
-    }
-  }
-
-  static class ExpandParameters extends SectionMatcher {
-    private final ParameterizedString template;
-    private final String prefix;
-
-    ExpandParameters(Project.NameKey project, String pattern,
-        AccessSection section) {
-      super(project, section);
-      template = new ParameterizedString(pattern);
-
-      if (isRE(pattern)) {
-        // Replace ${username} with ":USERNAME:" as : is not legal
-        // in a reference and the string :USERNAME: is not likely to
-        // be a valid part of the regex. This later allows the pattern
-        // prefix to be clipped, saving time on evaluation.
-        Automaton am = RefControl.toRegExp(
-            template.replace(Collections.singletonMap("username", ":USERNAME:")))
-            .toAutomaton();
-        String rePrefix = am.getCommonPrefix();
-        prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
-      } else {
-        prefix = pattern.substring(0, pattern.indexOf("${"));
-      }
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      if (!ref.startsWith(prefix) || username == null) {
-        return false;
-      }
-
-      String u;
-      if (isRE(template.getPattern())) {
-        u = username.replace(".", "\\.");
-      } else {
-        u = username;
-      }
-
-      SectionMatcher next = wrap(project,
-          template.replace(Collections.singletonMap("username", u)),
-          section);
-      return next != null ? next.match(ref, username) : false;
-    }
-
-   boolean matchPrefix(String ref) {
-     return ref.startsWith(prefix);
-    }
+  @Override
+  boolean match(String ref, String username) {
+    return this.matcher.match(ref, username);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index db879de..aeb92d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.project;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index 3e7a42f..2f8e26d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.SetHead.Input;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
@@ -40,10 +41,10 @@
   }
 
   private final GitRepositoryManager repoManager;
-  private final IdentifiedUser identifiedUser;
+  private final Provider<IdentifiedUser> identifiedUser;
 
   @Inject
-  SetHead(GitRepositoryManager repoManager, IdentifiedUser identifiedUser) {
+  SetHead(GitRepositoryManager repoManager, Provider<IdentifiedUser> identifiedUser) {
     this.repoManager = repoManager;
     this.identifiedUser = identifiedUser;
   }
@@ -73,7 +74,7 @@
 
       if (!repo.getRef(Constants.HEAD).getTarget().getName().equals(ref)) {
         final RefUpdate u = repo.updateRef(Constants.HEAD, true);
-        u.setRefLogIdent(identifiedUser.newRefLogIdent());
+        u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
         RefUpdate.Result res = u.link(ref);
         switch(res) {
           case NO_CHANGE:
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 c1fb5de..999358c 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
@@ -15,13 +15,16 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 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.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -33,6 +36,8 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
+import java.io.IOException;
+
 class SetParent implements RestModifyView<ProjectResource, Input> {
   static class Input {
     @DefaultInput
@@ -54,21 +59,45 @@
   }
 
   @Override
-  public String apply(ProjectResource resource, Input input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      Exception {
-    ProjectControl ctl = resource.getControl();
+  public String apply(final ProjectResource rsrc, Input input) throws AuthException,
+      BadRequestException, ResourceConflictException,
+      ResourceNotFoundException, UnprocessableEntityException, IOException {
+    ProjectControl ctl = rsrc.getControl();
     IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
     if (!user.getCapabilities().canAdministrateServer()) {
       throw new AuthException("not administrator");
     }
 
+    if (rsrc.getNameKey().equals(allProjects)) {
+      throw new ResourceConflictException("cannot set parent of "
+          + allProjects.get());
+    }
+
+    input.parent = Strings.emptyToNull(input.parent);
+    if (input.parent != null) {
+      ProjectState parent = cache.get(new Project.NameKey(input.parent));
+      if (parent == null) {
+        throw new UnprocessableEntityException("parent project " + input.parent
+            + " not found");
+      }
+
+      if (Iterables.tryFind(parent.tree(), new Predicate<ProjectState>() {
+        @Override
+        public boolean apply(ProjectState input) {
+          return input.getProject().getNameKey().equals(rsrc.getNameKey());
+        }
+      }).isPresent()) {
+        throw new ResourceConflictException("cycle exists between "
+            + rsrc.getName() + " and " + parent.getProject().getName());
+      }
+    }
+
     try {
-      MetaDataUpdate md = updateFactory.create(resource.getNameKey());
+      MetaDataUpdate md = updateFactory.create(rsrc.getNameKey());
       try {
         ProjectConfig config = ProjectConfig.read(md);
         Project project = config.getProject();
-        project.setParentName(Strings.emptyToNull(input.parent));
+        project.setParentName(input.parent);
 
         String msg = Strings.emptyToNull(input.commitMessage);
         if (msg == null) {
@@ -89,7 +118,7 @@
         md.close();
       }
     } catch (RepositoryNotFoundException notFound) {
-      throw new ResourceNotFoundException(resource.getName());
+      throw new ResourceNotFoundException(rsrc.getName());
     } catch (ConfigInvalidException e) {
       throw new ResourceConflictException(String.format(
           "invalid project.config: %s", e.getMessage()));
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 349567c..87161c5 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -34,8 +35,6 @@
 import java.util.Collections;
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 /**
  * Evaluates a submit-like Prolog rule found in the rules.pl file of the current
  * project and filters the results through rules found in the parent projects,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
index 8362b572..4fcc4a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
@@ -10,7 +10,7 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF 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;
+// limitations under the License.
 
 package com.google.gerrit.server.project;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 096e12e..5966dde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -45,9 +45,6 @@
         c += p.getCost();
       }
     }
-    if (t.size() < 2) {
-      throw new IllegalArgumentException("Need at least two predicates");
-    }
     children = t;
     cost = c;
   }
@@ -101,7 +98,7 @@
   }
 
   @Override
-  public final String toString() {
+  public String toString() {
     final StringBuilder r = new StringBuilder();
     r.append("(");
     for (int i = 0; i < getChildCount(); i++) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
deleted file mode 100644
index bea4da12..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query;
-
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-
-
-/** Predicate for a field of {@link ObjectId}. */
-public abstract class ObjectIdPredicate<T> extends OperatorPredicate<T> {
-  private final AbbreviatedObjectId id;
-
-  public ObjectIdPredicate(final String name, final AbbreviatedObjectId id) {
-    super(name, id.name());
-    this.id = id;
-  }
-
-  public boolean isComplete() {
-    return id.isComplete();
-  }
-
-  public AbbreviatedObjectId abbreviated() {
-    return id;
-  }
-
-  public ObjectId full() {
-    return id.toObjectId();
-  }
-
-  @Override
-  public int hashCode() {
-    return getOperator().hashCode() * 31 + id.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object other) {
-    if (other instanceof ObjectIdPredicate) {
-      final ObjectIdPredicate<?> p = (ObjectIdPredicate<?>) other;
-      return getOperator().equals(p.getOperator()) && id.equals(p.id);
-    }
-    return false;
-  }
-
-  @Override
-  public String toString() {
-    return getOperator() + ":" + id.name();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index 57d21b1..8a0ac68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -45,9 +45,6 @@
         c += p.getCost();
       }
     }
-    if (t.size() < 2) {
-      throw new IllegalArgumentException("Need at least two predicates");
-    }
     children = t;
     cost = c;
   }
@@ -101,7 +98,7 @@
   }
 
   @Override
-  public final String toString() {
+  public String toString() {
     final StringBuilder r = new StringBuilder();
     r.append("(");
     for (int i = 0; i < getChildCount(); i++) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index 5aa6cdc..a276992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -109,6 +109,56 @@
     }
   }
 
+  /**
+   * Locate a predicate in the predicate tree.
+   *
+   * @param p the predicate to find.
+   * @param clazz type of the predicate instance.
+   * @return the predicate, null if not found.
+   */
+  @SuppressWarnings("unchecked")
+  public static <T, P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
+    if (clazz.isAssignableFrom(p.getClass())) {
+      return (P) p;
+    }
+
+    for (Predicate<T> c : p.getChildren()) {
+      P r = find(c, clazz);
+      if (r != null) {
+        return r;
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Locate a predicate in the predicate tree.
+   *
+   * @param p the predicate to find.
+   * @param clazz type of the predicate instance.
+   * @param name name of the operator.
+   * @return the predicate, null if not found.
+   */
+  @SuppressWarnings("unchecked")
+  public static <T, P extends OperatorPredicate<T>> P find(Predicate<T> p,
+      Class<P> clazz, String name) {
+    if (p instanceof OperatorPredicate
+        && ((OperatorPredicate<?>) p).getOperator().equals(name)
+        && clazz.isAssignableFrom(p.getClass())) {
+      return (P) p;
+    }
+
+    for (Predicate<T> c : p.getChildren()) {
+      P r = find(c, clazz, name);
+      if (r != null) {
+        return r;
+      }
+    }
+
+    return null;
+  }
+
   @SuppressWarnings("rawtypes")
   private final Map<String, OperatorFactory> opFactories;
 
@@ -238,56 +288,6 @@
     throw error("Unsupported query:" + value);
   }
 
-  /**
-   * Locate a predicate in the predicate tree.
-   *
-   * @param p the predicate to find.
-   * @param clazz type of the predicate instance.
-   * @return the predicate, null if not found.
-   */
-  @SuppressWarnings("unchecked")
-  public <P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
-    if (clazz.isAssignableFrom(p.getClass())) {
-      return (P) p;
-    }
-
-    for (Predicate<T> c : p.getChildren()) {
-      P r = find(c, clazz);
-      if (r != null) {
-        return r;
-      }
-    }
-
-    return null;
-  }
-
-  /**
-   * Locate a predicate in the predicate tree.
-   *
-   * @param p the predicate to find.
-   * @param clazz type of the predicate instance.
-   * @param name name of the operator.
-   * @return the predicate, null if not found.
-   */
-  @SuppressWarnings("unchecked")
-  public <P extends OperatorPredicate<T>> P find(Predicate<T> p,
-      Class<P> clazz, String name) {
-    if (p instanceof OperatorPredicate
-        && ((OperatorPredicate<?>) p).getOperator().equals(name)
-        && clazz.isAssignableFrom(p.getClass())) {
-      return (P) p;
-    }
-
-    for (Predicate<T> c : p.getChildren()) {
-      P r = find(c, clazz, name);
-      if (r != null) {
-        return r;
-      }
-    }
-
-    return null;
-  }
-
   @SuppressWarnings("unchecked")
   private Predicate<T>[] children(final Tree r) throws QueryParseException,
       IllegalArgumentException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
index 2a6bae0..6088d8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.common.collect.Lists;
 import com.google.inject.name.Named;
 
 import java.lang.annotation.Annotation;
@@ -27,7 +28,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Comparator;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -65,19 +66,13 @@
     private final List<RewriteRule<T>> rewriteRules;
 
     public Definition(Class<R> clazz, QueryBuilder<T> qb) {
-      rewriteRules = new ArrayList<RewriteRule<T>>();
+      rewriteRules = Lists.newArrayList();
 
       Class<?> c = clazz;
       while (c != QueryRewriter.class) {
-        final Method[] declared = c.getDeclaredMethods();
-        Arrays.sort(declared, new Comparator<Method>() {
-          @Override
-          public int compare(Method o1, Method o2) {
-            return o1.getName().compareTo(o2.getName());
-          }
-        });
+        Method[] declared = c.getDeclaredMethods();
         for (Method m : declared) {
-          final Rewrite rp = m.getAnnotation(Rewrite.class);
+          Rewrite rp = m.getAnnotation(Rewrite.class);
           if ((m.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT
               && (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC
               && rp != null) {
@@ -86,6 +81,7 @@
         }
         c = c.getSuperclass();
       }
+      Collections.sort(rewriteRules);
     }
   }
 
@@ -120,13 +116,22 @@
     return Predicate.not(that);
   }
 
+  protected Predicate<T> preRewrite(Predicate<T> in) {
+    return in;
+  }
+
   /**
    * Apply rewrites to a graph until it stops changing.
    *
    * @param in the graph to rewrite.
    * @return the rewritten graph.
    */
-  public Predicate<T> rewrite(Predicate<T> in) {
+  public final Predicate<T> rewrite(Predicate<T> in) {
+    in = preRewrite(in);
+    return rewriteImpl(in);
+  }
+
+  private Predicate<T> rewriteImpl(Predicate<T> in) {
     Predicate<T> old;
     do {
       old = in;
@@ -135,7 +140,7 @@
       if (old.equals(in) && in.getChildCount() > 0) {
         List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
         for (Predicate<T> p : in.getChildren()) {
-          n.add(rewrite(p));
+          n.add(rewriteImpl(p));
         }
         n = removeDuplicates(n);
         if (n.size() == 1 && (isAND(in) || isOR(in))) {
@@ -150,17 +155,17 @@
   }
 
   protected Predicate<T> replaceGenericNodes(final Predicate<T> in) {
-    if (in.getClass() == NotPredicate.class) {
+    if (in instanceof NotPredicate) {
       return not(replaceGenericNodes(in.getChild(0)));
 
-    } else if (in.getClass() == AndPredicate.class) {
+    } else if (in instanceof AndPredicate) {
       List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
       for (Predicate<T> c : in.getChildren()) {
         n.add(replaceGenericNodes(c));
       }
       return and(n);
 
-    } else if (in.getClass() == OrPredicate.class) {
+    } else if (in instanceof OrPredicate) {
       List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
       for (Predicate<T> c : in.getChildren()) {
         n.add(replaceGenericNodes(c));
@@ -331,7 +336,7 @@
   }
 
   /** Applies a rewrite rule to a Predicate. */
-  protected interface RewriteRule<T> {
+  protected interface RewriteRule<T> extends Comparable<RewriteRule<T>> {
     /**
      * Apply a rewrite rule to the Predicate.
      *
@@ -454,6 +459,15 @@
       final String msg = "Cannot apply " + method.getName();
       return new IllegalArgumentException(msg, e);
     }
+
+    @Override
+    public int compareTo(RewriteRule<T> in) {
+      if (in instanceof MethodRewrite) {
+        return method.getName().compareTo(
+            ((MethodRewrite<T>) in).method.getName());
+      }
+      return 1;
+    }
   }
 
   private static <T> Predicate<T> removeDuplicates(Predicate<T> in) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index d0c9e4c..9a867d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -20,31 +20,43 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class AgePredicate extends OperatorPredicate<ChangeData> {
+import java.sql.Timestamp;
+
+public class AgePredicate extends TimestampRangePredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final long cut;
 
   AgePredicate(Provider<ReviewDb> dbProvider, String value) {
-    super(ChangeQueryBuilder.FIELD_AGE, value);
+    super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AGE, value);
     this.dbProvider = dbProvider;
 
     long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
     long ms = MILLISECONDS.convert(s, SECONDS);
-    this.cut = (System.currentTimeMillis() - ms) + 1;
+    this.cut = TimeUtil.nowMs() - ms;
+  }
+
+  public Timestamp getMinTimestamp() {
+    return new Timestamp(0);
+  }
+
+  public Timestamp getMaxTimestamp() {
+    return new Timestamp(cut);
   }
 
   long getCut() {
-    return cut;
+    return cut + 1;
   }
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
     Change change = object.change(dbProvider);
-    return change != null && change.getLastUpdatedOn().getTime() < cut;
+    return change != null && change.getLastUpdatedOn().getTime() <= cut;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index 7f726a7..5282b49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -34,7 +34,8 @@
 import java.util.Comparator;
 import java.util.List;
 
-class AndSource extends AndPredicate<ChangeData> implements ChangeDataSource {
+public class AndSource extends AndPredicate<ChangeData>
+    implements ChangeDataSource {
   private static final Comparator<Predicate<ChangeData>> CMP =
       new Comparator<Predicate<ChangeData>>() {
         @Override
@@ -75,7 +76,8 @@
   private final Provider<ReviewDb> db;
   private int cardinality = -1;
 
-  AndSource(Provider<ReviewDb> db, Collection<? extends Predicate<ChangeData>> that) {
+  public AndSource(Provider<ReviewDb> db,
+      Collection<? extends Predicate<ChangeData>> that) {
     super(sort(that));
     this.db = db;
   }
@@ -160,12 +162,15 @@
   }
 
   private ChangeDataSource source() {
+    int minCost = Integer.MAX_VALUE;
+    Predicate<ChangeData> s = null;
     for (Predicate<ChangeData> p : getChildren()) {
-      if (p instanceof ChangeDataSource) {
-        return (ChangeDataSource) p;
+      if (p instanceof ChangeDataSource && p.getCost() < minCost) {
+        s = p;
+        minCost = p.getCost();
       }
     }
-    return null;
+    return (ChangeDataSource) s;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
new file mode 100644
index 0000000..f81b0ca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+public abstract class BasicChangeRewrites extends QueryRewriter<ChangeData> {
+  protected static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
+      new ChangeQueryBuilder.Arguments( //
+          new InvalidProvider<ReviewDb>(), //
+          new InvalidProvider<ChangeQueryRewriter>(), //
+          null, null, null, null, null, //
+          null, null, null, null, null), null);
+
+  static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
+    ChangeIndex index = indexes != null ? indexes.getSearchIndex() : null;
+    return index != null ? index.getSchema() : null;
+  }
+
+  protected final Provider<ReviewDb> dbProvider;
+  private final IndexCollection indexes;
+
+  protected BasicChangeRewrites(
+      Definition<ChangeData, ? extends QueryRewriter<ChangeData>> def,
+      Provider<ReviewDb> dbProvider, IndexCollection indexes) {
+    super(def);
+    this.dbProvider = dbProvider;
+    this.indexes = indexes;
+  }
+
+  @Rewrite("-status:open")
+  @NoCostComputation
+  public Predicate<ChangeData> r00_notOpen() {
+    return ChangeStatusPredicate.closed(dbProvider);
+  }
+
+  @Rewrite("-status:closed")
+  @NoCostComputation
+  public Predicate<ChangeData> r00_notClosed() {
+    return ChangeStatusPredicate.open(dbProvider);
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("-status:merged")
+  public Predicate<ChangeData> r00_notMerged() {
+    return or(ChangeStatusPredicate.open(dbProvider),
+        new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED));
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("-status:abandoned")
+  public Predicate<ChangeData> r00_notAbandoned() {
+    return or(ChangeStatusPredicate.open(dbProvider),
+        new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
+  }
+
+  @SuppressWarnings("unchecked")
+  @NoCostComputation
+  @Rewrite("sortkey_before:z A=(age:*)")
+  public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
+    String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
+    return and(new SortKeyPredicate.Before(schema(indexes), dbProvider, cut), a);
+  }
+
+  @NoCostComputation
+  @Rewrite("A=(limit:*) B=(limit:*)")
+  public Predicate<ChangeData> r00_smallestLimit(
+      @Named("A") IntPredicate<ChangeData> a,
+      @Named("B") IntPredicate<ChangeData> b) {
+    return a.intValue() <= b.intValue() ? a : b;
+  }
+
+  @NoCostComputation
+  @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
+  public Predicate<ChangeData> r00_oldestSortKey(
+      @Named("A") SortKeyPredicate.Before a,
+      @Named("B") SortKeyPredicate.Before b) {
+    return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
+  }
+
+  @NoCostComputation
+  @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
+  public Predicate<ChangeData> r00_newestSortKey(
+      @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
+    return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
+  }
+
+  private static final class InvalidProvider<T> implements Provider<T> {
+    @Override
+    public T get() {
+      throw new OutOfScopeException("Not available at init");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
deleted file mode 100644
index e5b4143..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
-
-class BranchPredicate extends OperatorPredicate<ChangeData> {
-  private final Provider<ReviewDb> dbProvider;
-
-  BranchPredicate(Provider<ReviewDb> dbProvider, String branch) {
-    super(ChangeQueryBuilder.FIELD_BRANCH, branch.startsWith(Branch.R_HEADS)
-        ? branch : Branch.R_HEADS + branch);
-    this.dbProvider = dbProvider;
-  }
-
-  @Override
-  public boolean match(final ChangeData object) throws OrmException {
-    Change change = object.change(dbProvider);
-    if (change == null) {
-      return false;
-    }
-    return change.getDest().get().startsWith(Branch.R_HEADS)
-        && getValue().equals(change.getDest().get());
-  }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
-}
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 506fabc..91b2d9d 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.base.Function;
+import com.google.common.base.Objects;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
@@ -29,7 +30,6 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -43,15 +43,18 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 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 java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -72,8 +75,8 @@
     return SORT_APPROVALS.sortedCopy(approvals);
   }
 
-  public static void ensureChangeLoaded(
-      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+  public static void ensureChangeLoaded(Provider<ReviewDb> db,
+      Iterable<ChangeData> changes) throws OrmException {
     Map<Change.Id, ChangeData> missing = Maps.newHashMap();
     for (ChangeData cd : changes) {
       if (cd.change == null) {
@@ -87,8 +90,15 @@
     }
   }
 
-  public static void ensureCurrentPatchSetLoaded(
-      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+  public static void ensureAllPatchSetsLoaded(Provider<ReviewDb> db,
+      Iterable<ChangeData> changes) throws OrmException {
+    for (ChangeData cd : changes) {
+      cd.patches(db);
+    }
+  }
+
+  public static void ensureCurrentPatchSetLoaded(Provider<ReviewDb> db,
+      Iterable<ChangeData> changes) throws OrmException {
     Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
     for (ChangeData cd : changes) {
       if (cd.currentPatchSet == null && cd.patches == null) {
@@ -106,8 +116,8 @@
     }
   }
 
-  public static void ensureCurrentApprovalsLoaded(
-      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+  public static void ensureCurrentApprovalsLoaded(Provider<ReviewDb> db,
+      Iterable<ChangeData> changes) throws OrmException {
     List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
     for (ChangeData cd : changes) {
       if (cd.currentApprovals == null && cd.limitedApprovals == null) {
@@ -126,21 +136,24 @@
   }
 
   private final Change.Id legacyId;
+  private ChangeDataSource returnedBySource;
   private Change change;
   private String commitMessage;
+  private List<FooterLine> commitFooters;
   private PatchSet currentPatchSet;
   private Set<PatchSet.Id> limitedIds;
   private Collection<PatchSet> patches;
   private ListMultimap<PatchSet.Id, PatchSetApproval> limitedApprovals;
   private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
   private List<PatchSetApproval> currentApprovals;
-  private String[] currentFiles;
+  private List<String> currentFiles;
   private Collection<PatchLineComment> comments;
   private Collection<TrackingId> trackingIds;
   private CurrentUser visibleTo;
   private ChangeControl changeControl;
   private List<ChangeMessage> messages;
   private List<SubmitRecord> submitRecords;
+  private boolean patchesLoaded;
 
   public ChangeData(final Change.Id id) {
     legacyId = id;
@@ -157,6 +170,14 @@
     changeControl = c;
   }
 
+  public boolean isFromSource(ChangeDataSource s) {
+    return s == returnedBySource;
+  }
+
+  public void cacheFromSource(ChangeDataSource s) {
+    returnedBySource = s;
+  }
+
   public void limitToPatchSets(Collection<PatchSet.Id> ids) {
     limitedIds = Sets.newLinkedHashSetWithExpectedSize(ids.size());
     for (PatchSet.Id id : ids) {
@@ -172,11 +193,11 @@
     return limitedIds;
   }
 
-  public void setCurrentFilePaths(String[] filePaths) {
-    currentFiles = filePaths;
+  public void setCurrentFilePaths(List<String> filePaths) {
+    currentFiles = ImmutableList.copyOf(filePaths);
   }
 
-  public String[] currentFilePaths(Provider<ReviewDb> db,
+  public List<String> currentFilePaths(Provider<ReviewDb> db,
       PatchListCache cache) throws OrmException {
     if (currentFiles == null) {
       Change c = change(db);
@@ -192,7 +213,7 @@
       try {
         p = cache.get(c, ps);
       } catch (PatchListNotAvailableException e) {
-        currentFiles = new String[0];
+        currentFiles = Collections.emptyList();
         return currentFiles;
       }
 
@@ -206,6 +227,7 @@
           case MODIFIED:
           case DELETED:
           case COPIED:
+          case REWRITE:
             r.add(e.getNewName());
             break;
 
@@ -213,13 +235,10 @@
             r.add(e.getOldName());
             r.add(e.getNewName());
             break;
-
-          case REWRITE:
-            break;
         }
       }
-      currentFiles = r.toArray(new String[r.size()]);
-      Arrays.sort(currentFiles);
+      Collections.sort(r);
+      currentFiles = Collections.unmodifiableList(r);
     }
     return currentFiles;
   }
@@ -256,6 +275,10 @@
     return change;
   }
 
+  void setChange(Change c) {
+    change = c;
+  }
+
   public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
     if (currentPatchSet == null) {
       Change c = change(db);
@@ -291,28 +314,46 @@
     return currentApprovals;
   }
 
+  public void setCurrentApprovals(List<PatchSetApproval> approvals) {
+    currentApprovals = approvals;
+  }
+
   public String commitMessage(GitRepositoryManager repoManager,
       Provider<ReviewDb> db) throws IOException, OrmException {
     if (commitMessage == null) {
-      PatchSet.Id psId = change(db).currentPatchSetId();
-      String sha1 = db.get().patchSets().get(psId).getRevision().get();
-      Project.NameKey name = change.getProject();
-      Repository repo = repoManager.openRepository(name);
-      try {
-        RevWalk walk = new RevWalk(repo);
-        try {
-          RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
-          commitMessage = c.getFullMessage();
-        } finally {
-          walk.release();
-        }
-      } finally {
-        repo.close();
-      }
+      loadCommitData(repoManager, db);
     }
     return commitMessage;
   }
 
+  public List<FooterLine> commitFooters(GitRepositoryManager repoManager,
+      Provider<ReviewDb> db) throws IOException, OrmException {
+    if (commitFooters == null) {
+      loadCommitData(repoManager, db);
+    }
+    return commitFooters;
+  }
+
+  private void loadCommitData(GitRepositoryManager repoManager,
+      Provider<ReviewDb> db) throws OrmException, RepositoryNotFoundException,
+      IOException, MissingObjectException, IncorrectObjectTypeException {
+    PatchSet.Id psId = change(db).currentPatchSetId();
+    String sha1 = db.get().patchSets().get(psId).getRevision().get();
+    Repository repo = repoManager.openRepository(change.getProject());
+    try {
+      RevWalk walk = new RevWalk(repo);
+      try {
+        RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
+        commitMessage = c.getFullMessage();
+        commitFooters = c.getFooterLines();
+      } finally {
+        walk.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
   /**
    * @param db review database.
    * @return patches for the change. If {@link #limitToPatchSets(Collection)}
@@ -321,7 +362,7 @@
    */
   public Collection<PatchSet> patches(Provider<ReviewDb> db)
       throws OrmException {
-    if (patches == null) {
+    if (patches == null || !patchesLoaded) {
       if (limitedIds != null) {
         patches = Lists.newArrayList();
         for (PatchSet ps : db.get().patchSets().byChange(legacyId)) {
@@ -332,6 +373,7 @@
       } else {
         patches = db.get().patchSets().byChange(legacyId).toList();
       }
+      patchesLoaded = true;
     }
     return patches;
   }
@@ -438,4 +480,9 @@
   public List<SubmitRecord> getSubmitRecords() {
     return submitRecords;
   }
+
+  @Override
+  public String toString() {
+    return Objects.toStringHelper(this).addValue(getId()).toString();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index 7116aa9..457d657 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -16,17 +16,18 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
-class ChangeIdPredicate extends OperatorPredicate<ChangeData> implements
+class ChangeIdPredicate extends IndexPredicate<ChangeData> implements
     ChangeDataSource {
   private final Provider<ReviewDb> dbProvider;
 
   ChangeIdPredicate(Provider<ReviewDb> dbProvider, String id) {
-    super(ChangeQueryBuilder.FIELD_CHANGE, id);
+    super(ChangeField.ID, ChangeQueryBuilder.FIELD_CHANGE, id);
     this.dbProvider = dbProvider;
   }
 
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 e74172e..9fc3dbf 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
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -30,6 +32,9 @@
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
@@ -49,6 +54,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.regex.Pattern;
 
@@ -76,6 +82,7 @@
   public static final String FIELD_AGE = "age";
   public static final String FIELD_BRANCH = "branch";
   public static final String FIELD_CHANGE = "change";
+  public static final String FIELD_COMMENT = "comment";
   public static final String FIELD_COMMIT = "commit";
   public static final String FIELD_DRAFTBY = "draftby";
   public static final String FIELD_FILE = "file";
@@ -97,11 +104,38 @@
   public static final String FIELD_VISIBLETO = "visibleto";
   public static final String FIELD_WATCHEDBY = "watchedby";
 
+  public static final String ARG_ID_USER = "user";
+  public static final String ARG_ID_GROUP = "group";
+
+
   private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
       new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
           ChangeQueryBuilder.class);
 
-  static class Arguments {
+  @SuppressWarnings("unchecked")
+  public static boolean hasLimit(Predicate<ChangeData> p) {
+    return find(p, IntPredicate.class, FIELD_LIMIT) != null;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static int getLimit(Predicate<ChangeData> p) {
+    return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
+  }
+
+  public static boolean hasNonTrivialSortKeyAfter(Schema<ChangeData> schema,
+      Predicate<ChangeData> p) {
+    SortKeyPredicate after =
+        (SortKeyPredicate) find(p, SortKeyPredicate.class, "sortkey_after");
+    return after != null && after.getMaxValue(schema) > 0;
+  }
+
+  public static boolean hasSortKey(Predicate<ChangeData> p) {
+    return find(p, SortKeyPredicate.class, "sortkey_after") != null
+        || find(p, SortKeyPredicate.class, "sortkey_before") != null;
+  }
+
+  @VisibleForTesting
+  public static class Arguments {
     final Provider<ReviewDb> dbProvider;
     final Provider<ChangeQueryRewriter> rewriter;
     final IdentifiedUser.GenericFactory userFactory;
@@ -113,9 +147,11 @@
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
     final ProjectCache projectCache;
+    final IndexCollection indexes;
 
     @Inject
-    Arguments(Provider<ReviewDb> dbProvider,
+    @VisibleForTesting
+    public Arguments(Provider<ReviewDb> dbProvider,
         Provider<ChangeQueryRewriter> rewriter,
         IdentifiedUser.GenericFactory userFactory,
         CapabilityControl.Factory capabilityControlFactory,
@@ -125,7 +161,8 @@
         AllProjectsName allProjectsName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
-        ProjectCache projectCache) {
+        ProjectCache projectCache,
+        IndexCollection indexes) {
       this.dbProvider = dbProvider;
       this.rewriter = rewriter;
       this.userFactory = userFactory;
@@ -137,6 +174,7 @@
       this.patchListCache = patchListCache;
       this.repoManager = repoManager;
       this.projectCache = projectCache;
+      this.indexes = indexes;
     }
   }
 
@@ -146,17 +184,26 @@
 
   private final Arguments args;
   private final CurrentUser currentUser;
-  private boolean allowsFile;
+  private boolean allowFileRegex;
 
   @Inject
-  ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
+  public ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
     super(mydef);
     this.args = args;
     this.currentUser = currentUser;
   }
 
-  public void setAllowFile(boolean on) {
-    allowsFile = on;
+  @VisibleForTesting
+  protected ChangeQueryBuilder(
+      QueryBuilder.Definition<ChangeData, ? extends ChangeQueryBuilder> def,
+      Arguments args, CurrentUser currentUser) {
+    super(def);
+    this.args = args;
+    this.currentUser = currentUser;
+  }
+
+  public void setAllowFileRegex(boolean on) {
+    allowFileRegex = on;
   }
 
   @Operator
@@ -181,6 +228,12 @@
   }
 
   @Operator
+  public Predicate<ChangeData> comment(String value) throws QueryParseException {
+    ChangeIndex index = requireIndex(FIELD_COMMENT, value);
+    return new CommentPredicate(args.dbProvider, index, value);
+  }
+
+  @Operator
   public Predicate<ChangeData> status(String statusName) {
     if ("open".equals(statusName)) {
       return status_open();
@@ -220,7 +273,7 @@
     }
 
     if ("watched".equalsIgnoreCase(value)) {
-      return new IsWatchedByPredicate(args, currentUser);
+      return new IsWatchedByPredicate(args, currentUser, false);
     }
 
     if ("visible".equalsIgnoreCase(value)) {
@@ -264,8 +317,14 @@
   @Operator
   public Predicate<ChangeData> branch(String name) {
     if (name.startsWith("^"))
-      return new RegexBranchPredicate(args.dbProvider, name);
-    return new BranchPredicate(args.dbProvider, name);
+      return ref("^" + branchToRef(name.substring(1)));
+    return ref(branchToRef(name));
+  }
+
+  private static String branchToRef(String name) {
+    if (!name.startsWith(Branch.R_HEADS))
+      return Branch.R_HEADS + name;
+    return name;
   }
 
   @Operator
@@ -284,27 +343,79 @@
 
   @Operator
   public Predicate<ChangeData> file(String file) throws QueryParseException {
-    if (!allowsFile) {
-      throw error("operator not permitted here: file:" + file);
-    }
-
     if (file.startsWith("^")) {
+      if (!allowFileRegex) {
+        requireIndex(FIELD_FILE, file);
+      }
       return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
+    } else {
+      requireIndex(FIELD_FILE, file);
+      return new EqualsFilePredicate(args.dbProvider, args.patchListCache, file);
     }
-
-    throw new IllegalArgumentException();
   }
 
   @Operator
-  public Predicate<ChangeData> label(String name) {
+  public Predicate<ChangeData> label(String name) throws QueryParseException,
+      OrmException {
+    Set<Account.Id> accounts = null;
+    AccountGroup.UUID group = null;
+
+    // Parse for:
+    // label:CodeReview=1,user=jsmith or
+    // label:CodeReview=1,jsmith or
+    // label:CodeReview=1,group=android_approvers or
+    // label:CodeReview=1,android_approvers
+    //  user/groups without a label will first attempt to match user
+    String[] splitReviewer = name.split(",", 2);
+    name = splitReviewer[0];        // remove all but the vote piece, e.g.'CodeReview=1'
+
+    if (splitReviewer.length == 2) {
+      // process the user/group piece
+      PredicateArgs lblArgs = new PredicateArgs(splitReviewer[1]);
+
+      for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
+        if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
+          accounts = parseAccount(pair.getValue());
+        } else if (pair.getKey().equalsIgnoreCase(ARG_ID_GROUP)) {
+          group = parseGroup(pair.getValue()).getUUID();
+        } else {
+          throw new QueryParseException(
+              "Invalid argument identifier '"   + pair.getKey() + "'");
+        }
+      }
+
+      for (String value : lblArgs.positional) {
+       if (accounts != null || group != null) {
+          throw new QueryParseException("more than one user/group specified (" +
+              value + ")");
+        }
+        try {
+          accounts = parseAccount(value);
+        } catch (QueryParseException qpex) {
+          // If it doesn't match an account, see if it matches a group
+          // (accounts get precedence)
+          try {
+            group = parseGroup(value).getUUID();
+          } catch (QueryParseException e) {
+            throw error("Neither user nor group " + value + " found");
+          }
+        }
+      }
+    }
+
     return new LabelPredicate(args.projectCache,
         args.changeControlGenericFactory, args.userFactory, args.dbProvider,
-        name);
+        name, accounts, group);
   }
 
   @Operator
-  public Predicate<ChangeData> message(String text) {
-    return new MessagePredicate(args.dbProvider, args.repoManager, text);
+  public Predicate<ChangeData> message(String text) throws QueryParseException {
+    ChangeIndex index = args.indexes.getSearchIndex();
+    if (index == null) {
+      return new LegacyMessagePredicate(args.dbProvider, args.repoManager, text);
+    }
+
+    return new MessagePredicate(args.dbProvider, index, text);
   }
 
   @Operator
@@ -328,12 +439,12 @@
     Set<Account.Id> m = parseAccount(who);
     List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
     for (Account.Id id : m) {
-      if (currentUser instanceof IdentifiedUser
+      if (currentUser.isIdentifiedUser()
           && id.equals(((IdentifiedUser) currentUser).getAccountId())) {
-        p.add(new IsWatchedByPredicate(args, currentUser));
+        p.add(new IsWatchedByPredicate(args, currentUser, false));
       } else {
         p.add(new IsWatchedByPredicate(args,
-            args.userFactory.create(args.dbProvider, id)));
+            args.userFactory.create(args.dbProvider, id), true));
       }
     }
     return Predicate.or(p);
@@ -446,28 +557,36 @@
     return limit(Integer.parseInt(limit));
   }
 
-  public Predicate<ChangeData> limit(int limit) {
-    return new IntPredicate<ChangeData>(FIELD_LIMIT, limit) {
-      @Override
-      public boolean match(ChangeData object) {
-        return true;
-      }
+  static class LimitPredicate extends IntPredicate<ChangeData> {
+    LimitPredicate(int limit) {
+      super(FIELD_LIMIT, limit);
+    }
 
-      @Override
-      public int getCost() {
-        return 0;
-      }
-    };
+    @Override
+    public boolean match(ChangeData object) {
+      return true;
+    }
+
+    @Override
+    public int getCost() {
+      return 0;
+    }
+  }
+
+  public Predicate<ChangeData> limit(int limit) {
+    return new LimitPredicate(limit);
   }
 
   @Operator
   public Predicate<ChangeData> sortkey_after(String sortKey) {
-    return new SortKeyPredicate.After(args.dbProvider, sortKey);
+    return new SortKeyPredicate.After(
+        BasicChangeRewrites.schema(args.indexes), args.dbProvider, sortKey);
   }
 
   @Operator
   public Predicate<ChangeData> sortkey_before(String sortKey) {
-    return new SortKeyPredicate.Before(args.dbProvider, sortKey);
+    return new SortKeyPredicate.Before(
+        BasicChangeRewrites.schema(args.indexes), args.dbProvider, sortKey);
   }
 
   @Operator
@@ -476,21 +595,6 @@
   }
 
   @SuppressWarnings("unchecked")
-  public boolean hasLimit(Predicate<ChangeData> p) {
-    return find(p, IntPredicate.class, FIELD_LIMIT) != null;
-  }
-
-  @SuppressWarnings("unchecked")
-  public int getLimit(Predicate<ChangeData> p) {
-    return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
-  }
-
-  public boolean hasSortKey(Predicate<ChangeData> p) {
-    return find(p, SortKeyPredicate.class, "sortkey_after") != null
-        || find(p, SortKeyPredicate.class, "sortkey_before") != null;
-  }
-
-  @SuppressWarnings("unchecked")
   @Override
   protected Predicate<ChangeData> defaultField(String query)
       throws QueryParseException {
@@ -511,7 +615,11 @@
       }
 
     } else if (PAT_LABEL.matcher(query).find()) {
-      return label(query);
+      try {
+        return label(query);
+      } catch (OrmException err) {
+        throw error("Cannot lookup user", err);
+      }
 
     } else {
       // Try to match a project name by substring query.
@@ -548,10 +656,28 @@
     return matches;
   }
 
+  private GroupReference parseGroup(String group) throws QueryParseException {
+    GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend,
+        group);
+    if (g == null) {
+      throw error("Group " + group + " not found");
+    }
+    return g;
+  }
+
   private Account.Id self() {
-    if (currentUser instanceof IdentifiedUser) {
+    if (currentUser.isIdentifiedUser()) {
       return ((IdentifiedUser) currentUser).getAccountId();
     }
     throw new IllegalArgumentException();
   }
+
+  private ChangeIndex requireIndex(String field, String value)
+      throws QueryParseException {
+    ChangeIndex idx = args.indexes.getSearchIndex();
+    if (idx == null) {
+      throw error("secondary index must be enabled for " + field + ":" + value);
+    }
+    return idx;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index e6251bc..bd186c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2013 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,693 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryRewriter;
-import com.google.gerrit.server.query.RewritePredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.name.Named;
+import com.google.gerrit.server.query.QueryParseException;
 
-import java.util.Collection;
-
-public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
-  private static final QueryRewriter.Definition<ChangeData, ChangeQueryRewriter> mydef =
-      new QueryRewriter.Definition<ChangeData, ChangeQueryRewriter>(
-          ChangeQueryRewriter.class, new ChangeQueryBuilder(
-              new ChangeQueryBuilder.Arguments( //
-                  new InvalidProvider<ReviewDb>(), //
-                  new InvalidProvider<ChangeQueryRewriter>(), //
-                  null, null, null, null, null, //
-                  null, null, null, null), null));
-
-  private final Provider<ReviewDb> dbProvider;
-
-  @Inject
-  ChangeQueryRewriter(Provider<ReviewDb> dbProvider) {
-    super(mydef);
-    this.dbProvider = dbProvider;
-  }
-
-  @Override
-  public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
-    return hasSource(l) ? new AndSource(dbProvider, l) : super.and(l);
-  }
-
-  @Override
-  public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
-    return hasSource(l) ? new OrSource(l) : super.or(l);
-  }
-
-  @Rewrite("-status:open")
-  @NoCostComputation
-  public Predicate<ChangeData> r00_notOpen() {
-    return ChangeStatusPredicate.closed(dbProvider);
-  }
-
-  @Rewrite("-status:closed")
-  @NoCostComputation
-  public Predicate<ChangeData> r00_notClosed() {
-    return ChangeStatusPredicate.open(dbProvider);
-  }
-
-  @SuppressWarnings("unchecked")
-  @NoCostComputation
-  @Rewrite("-status:merged")
-  public Predicate<ChangeData> r00_notMerged() {
-    return or(ChangeStatusPredicate.open(dbProvider),
-        new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED));
-  }
-
-  @SuppressWarnings("unchecked")
-  @NoCostComputation
-  @Rewrite("-status:abandoned")
-  public Predicate<ChangeData> r00_notAbandoned() {
-    return or(ChangeStatusPredicate.open(dbProvider),
-        new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
-  }
-
-  @SuppressWarnings("unchecked")
-  @NoCostComputation
-  @Rewrite("sortkey_before:z A=(age:*)")
-  public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
-    String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
-    return and(new SortKeyPredicate.Before(dbProvider, cut), a);
-  }
-
-  @NoCostComputation
-  @Rewrite("A=(limit:*) B=(limit:*)")
-  public Predicate<ChangeData> r00_smallestLimit(
-      @Named("A") IntPredicate<ChangeData> a,
-      @Named("B") IntPredicate<ChangeData> b) {
-    return a.intValue() <= b.intValue() ? a : b;
-  }
-
-  @NoCostComputation
-  @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
-  public Predicate<ChangeData> r00_oldestSortKey(
-      @Named("A") SortKeyPredicate.Before a,
-      @Named("B") SortKeyPredicate.Before b) {
-    return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
-  }
-
-  @NoCostComputation
-  @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
-  public Predicate<ChangeData> r00_newestSortKey(
-      @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
-    return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
-  }
-
-  @Rewrite("status:open P=(project:*) B=(branch:*)")
-  public Predicate<ChangeData> r05_byBranchOpen(
-      @Named("P") final ProjectPredicate p,
-      @Named("B") final BranchPredicate b) {
-    return new ChangeSource(500) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a)
-          throws OrmException {
-        return a.byBranchOpenAll(
-            new Branch.NameKey(p.getValueKey(), b.getValue()));
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen()
-            && p.match(cd)
-            && b.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:merged P=(project:*) B=(branch:*) S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r05_byBranchMergedPrev(
-      @Named("P") final ProjectPredicate p,
-      @Named("B") final BranchPredicate b,
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byBranchClosedPrev(Change.Status.MERGED.getCode(), //
-            new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
-            && p.match(cd) //
-            && b.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:merged P=(project:*) B=(branch:*) S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r05_byBranchMergedNext(
-      @Named("P") final ProjectPredicate p,
-      @Named("B") final BranchPredicate b,
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byBranchClosedNext(Change.Status.MERGED.getCode(), //
-            new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
-            && p.match(cd) //
-            && b.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r10_byProjectOpenPrev(
-      @Named("P") final ProjectPredicate p,
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(500, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byProjectOpenPrev(p.getValueKey(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen() //
-            && p.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r10_byProjectOpenNext(
-      @Named("P") final ProjectPredicate p,
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(500, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byProjectOpenNext(p.getValueKey(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen() //
-            && p.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r10_byProjectMergedPrev(
-      @Named("P") final ProjectPredicate p,
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), //
-            p.getValueKey(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
-            && p.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r10_byProjectMergedNext(
-      @Named("P") final ProjectPredicate p,
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byProjectClosedNext(Change.Status.MERGED.getCode(), //
-            p.getValueKey(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
-            && p.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r10_byProjectAbandonedPrev(
-      @Named("P") final ProjectPredicate p,
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), //
-            p.getValueKey(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
-            && p.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r10_byProjectAbandonedNext(
-      @Named("P") final ProjectPredicate p,
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), //
-            p.getValueKey(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
-            && p.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byOpenPrev(
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.allOpenPrev(key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byOpenNext(
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.allOpenNext(key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byMergedPrev(
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
-      {
-        init("r20_byMergedPrev", s, l);
-      }
-
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
-            && s.match(cd);
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byMergedNext(
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
-      {
-        init("r20_byMergedNext", s, l);
-      }
-
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
-            && s.match(cd);
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byAbandonedPrev(
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
-      {
-        init("r20_byAbandonedPrev", s, l);
-      }
-
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
-            && s.match(cd);
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byAbandonedNext(
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
-      {
-        init("r20_byAbandonedNext", s, l);
-      }
-
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit);
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
-            && s.match(cd);
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byClosedPrev(
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l));
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r20_byClosedNext(
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l));
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:open O=(owner:*)")
-  public Predicate<ChangeData> r25_byOwnerOpen(
-      @Named("O") final OwnerPredicate o) {
-    return new ChangeSource(50) {
-      {
-        init("r25_byOwnerOpen", o);
-      }
-
-      @Override
-      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
-        return a.byOwnerOpen(o.getAccountId());
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen() && o.match(cd);
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:closed O=(owner:*)")
-  public Predicate<ChangeData> r25_byOwnerClosed(
-      @Named("O") final OwnerPredicate o) {
-    return new ChangeSource(5000) {
-      {
-        init("r25_byOwnerClosed", o);
-      }
-
-      @Override
-      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
-        return a.byOwnerClosedAll(o.getAccountId());
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isClosed() && o.match(cd);
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("O=(owner:*)")
-  public Predicate<ChangeData> r26_byOwner(@Named("O") OwnerPredicate o) {
-    return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o));
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:open R=(reviewer:*)")
-  public Predicate<ChangeData> r30_byReviewerOpen(
-      @Named("R") final ReviewerPredicate r) {
-    return new Source() {
-      {
-        init("r30_byReviewerOpen", r);
-      }
-
-      @Override
-      public ResultSet<ChangeData> read() throws OrmException {
-        return ChangeDataResultSet.patchSetApproval(dbProvider.get()
-            .patchSetApprovals().openByUser(r.getAccountId()));
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        Change change = cd.change(dbProvider);
-        return change != null && change.getStatus().isOpen() && r.match(cd);
-      }
-
-      @Override
-      public int getCardinality() {
-        return 50;
-      }
-
-      @Override
-      public int getCost() {
-        return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("status:closed R=(reviewer:*)")
-  public Predicate<ChangeData> r30_byReviewerClosed(
-      @Named("R") final ReviewerPredicate r) {
-    return new Source() {
-      {
-        init("r30_byReviewerClosed", r);
-      }
-
-      @Override
-      public ResultSet<ChangeData> read() throws OrmException {
-        return ChangeDataResultSet.patchSetApproval(dbProvider.get()
-            .patchSetApprovals().closedByUserAll(r.getAccountId()));
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        Change change = cd.change(dbProvider);
-        return change != null && change.getStatus().isClosed() && r.match(cd);
-      }
-
-      @Override
-      public int getCardinality() {
-        return 5000;
-      }
-
-      @Override
-      public int getCost() {
-        return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
-      }
-    };
-  }
-
-  @SuppressWarnings("unchecked")
-  @Rewrite("R=(reviewer:*)")
-  public Predicate<ChangeData> r31_byReviewer(
-      @Named("R") final ReviewerPredicate r) {
-    return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r));
-  }
-
-  @Rewrite("status:submitted")
-  public Predicate<ChangeData> r99_allSubmitted() {
-    return new ChangeSource(50) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
-        return a.allSubmitted();
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED;
-      }
-    };
-  }
-
-  @Rewrite("P=(project:*)")
-  public Predicate<ChangeData> r99_byProject(
-      @Named("P") final ProjectPredicate p) {
-    return new ChangeSource(1000000) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
-        return a.byProject(p.getValueKey());
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return p.match(cd);
-      }
-    };
-  }
-
-  private static boolean hasSource(Collection<? extends Predicate<ChangeData>> l) {
-    for (Predicate<ChangeData> p : l) {
-      if (p instanceof ChangeDataSource) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private abstract static class Source extends RewritePredicate<ChangeData>
-      implements ChangeDataSource {
-    @Override
-    public boolean hasChange() {
-      return false;
-    }
-  }
-
-  private abstract class ChangeSource extends Source {
-    private final int cardinality;
-
-    ChangeSource(int card) {
-      this.cardinality = card;
-    }
-
-    abstract ResultSet<Change> scan(ChangeAccess a) throws OrmException;
-
-    @Override
-    public ResultSet<ChangeData> read() throws OrmException {
-      return ChangeDataResultSet.change(scan(dbProvider.get().changes()));
-    }
-
-    @Override
-    public boolean hasChange() {
-      return true;
-    }
-
-    @Override
-    public int getCardinality() {
-      return cardinality;
-    }
-
-    @Override
-    public int getCost() {
-      return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
-    }
-  }
-
-  private abstract class PaginatedSource extends ChangeSource implements
-      Paginated {
-    private final String startKey;
-    private final int limit;
-
-    PaginatedSource(int card, String start, int lim) {
-      super(card);
-      this.startKey = start;
-      this.limit = lim;
-    }
-
-    @Override
-    public int limit() {
-      return limit;
-    }
-
-    @Override
-    ResultSet<Change> scan(ChangeAccess a) throws OrmException {
-      return scan(a, startKey, limit);
-    }
-
-    @Override
-    public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
-      return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
-          last.change(dbProvider).getSortKey(), //
-          limit));
-    }
-
-    abstract ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-        throws OrmException;
-  }
-
-  private static final class InvalidProvider<T> implements Provider<T> {
-    @Override
-    public T get() {
-      throw new OutOfScopeException("Not available at init");
-    }
-  }
+public interface ChangeQueryRewriter {
+  Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+      throws QueryParseException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 6e9e79c..d5d6a92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -14,19 +14,19 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableBiMap;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-
 
 /**
  * Predicate for a {@link Change.Status}.
@@ -35,21 +35,19 @@
  * status:} but may also be {@code is:} to help do-what-i-meanery for end-users
  * searching for changes. Either operator name has the same meaning.
  */
-final class ChangeStatusPredicate extends OperatorPredicate<ChangeData> {
-  private static final Map<String, Change.Status> byName;
-  private static final EnumMap<Change.Status, String> byEnum;
+public final class ChangeStatusPredicate extends IndexPredicate<ChangeData> {
+  public static final ImmutableBiMap<Change.Status, String> VALUES;
 
   static {
-    byName = new HashMap<String, Change.Status>();
-    byEnum = new EnumMap<Change.Status, String>(Change.Status.class);
-    for (final Change.Status s : Change.Status.values()) {
-      final String name = s.name().toLowerCase();
-      byName.put(name, s);
-      byEnum.put(s, name);
+    ImmutableBiMap.Builder<Change.Status, String> values =
+        ImmutableBiMap.builder();
+    for (Change.Status s : Change.Status.values()) {
+      values.put(s, s.name().toLowerCase());
     }
+    VALUES = values.build();
   }
 
-  static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
+  public static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
     List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
     for (final Change.Status e : Change.Status.values()) {
       if (e.isOpen()) {
@@ -59,7 +57,7 @@
     return r.size() == 1 ? r.get(0) : or(r);
   }
 
-  static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
+  public static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
     List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
     for (final Change.Status e : Change.Status.values()) {
       if (e.isClosed()) {
@@ -69,28 +67,23 @@
     return r.size() == 1 ? r.get(0) : or(r);
   }
 
-  private static Change.Status parse(final String value) {
-    final Change.Status s = byName.get(value);
-    if (s == null) {
-      throw new IllegalArgumentException();
-    }
-    return s;
-  }
-
   private final Provider<ReviewDb> dbProvider;
   private final Change.Status status;
 
   ChangeStatusPredicate(Provider<ReviewDb> dbProvider, String value) {
-    this(dbProvider, parse(value));
+    super(ChangeField.STATUS, value);
+    this.dbProvider = dbProvider;
+    status = VALUES.inverse().get(value);
+    checkArgument(status != null, "invalid change status: %s", value);
   }
 
   ChangeStatusPredicate(Provider<ReviewDb> dbProvider, Change.Status status) {
-    super(ChangeQueryBuilder.FIELD_STATUS, byEnum.get(status));
+    super(ChangeField.STATUS, VALUES.get(status));
     this.dbProvider = dbProvider;
     this.status = status;
   }
 
-  Change.Status getStatus() {
+  public Change.Status getStatus() {
     return status;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
new file mode 100644
index 0000000..563e37b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+class CommentPredicate extends IndexPredicate<ChangeData> {
+  private final Provider<ReviewDb> db;
+  private final ChangeIndex index;
+
+  CommentPredicate(Provider<ReviewDb> db, ChangeIndex index, String value) {
+    super(ChangeField.COMMENT, value);
+    this.db = db;
+    this.index = index;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    try {
+      for (ChangeData cData : index.getSource(
+          Predicate.and(new LegacyChangeIdPredicate(db, object.getId()), this), 1)
+          .read()) {
+        if (cData.getId().equals(object.getId())) {
+          return true;
+        }
+      }
+    } catch (QueryParseException e) {
+      throw new OrmException(e);
+    }
+
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 274b40c..082dc83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -17,7 +17,8 @@
 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.query.ObjectIdPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
@@ -25,13 +26,15 @@
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
-class CommitPredicate extends ObjectIdPredicate<ChangeData> implements
+class CommitPredicate extends IndexPredicate<ChangeData> implements
     ChangeDataSource {
   private final Provider<ReviewDb> dbProvider;
+  private final AbbreviatedObjectId abbrevId;
 
   CommitPredicate(Provider<ReviewDb> dbProvider, AbbreviatedObjectId id) {
-    super(ChangeQueryBuilder.FIELD_COMMIT, id);
+    super(ChangeField.COMMIT, id.name());
     this.dbProvider = dbProvider;
+    this.abbrevId = id;
   }
 
   @Override
@@ -39,7 +42,7 @@
     for (PatchSet p : object.patches(dbProvider)) {
       if (p.getRevision() != null && p.getRevision().get() != null) {
         final ObjectId id = ObjectId.fromString(p.getRevision().get());
-        if (abbreviated().prefixCompare(id) == 0) {
+        if (abbrevId.prefixCompare(id) == 0) {
           return true;
         }
       }
@@ -49,7 +52,7 @@
 
   @Override
   public ResultSet<ChangeData> read() throws OrmException {
-    final RevId id = new RevId(abbreviated().name());
+    final RevId id = new RevId(abbrevId.name());
     if (id.isComplete()) {
       return ChangeDataResultSet.patchSet(//
           dbProvider.get().patchSets().byRevision(id));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
new file mode 100644
index 0000000..002dc99
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+import java.util.List;
+
+class EqualsFilePredicate extends IndexPredicate<ChangeData> {
+  private final Provider<ReviewDb> db;
+  private final PatchListCache cache;
+  private final String value;
+
+  EqualsFilePredicate(Provider<ReviewDb> db, PatchListCache plc, String value) {
+    super(ChangeField.FILE, value);
+    this.db = db;
+    this.cache = plc;
+    this.value = value;
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    List<String> files = object.currentFilePaths(db, cache);
+    if (files != null) {
+      return Collections.binarySearch(files, value) >= 0;
+    } else {
+      // The ChangeData can't do expensive lookups right now. Bypass
+      // them and include the result anyway. We might be able to do
+      // a narrow later on to a smaller set.
+      //
+      return true;
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
new file mode 100644
index 0000000..af9a07a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+class EqualsLabelPredicate extends IndexPredicate<ChangeData> {
+  private final ProjectCache projectCache;
+  private final ChangeControl.GenericFactory ccFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final String label;
+  private final int expVal;
+  private final Account.Id account;
+  private final AccountGroup.UUID group;
+
+  EqualsLabelPredicate(ProjectCache projectCache,
+      ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String label, int expVal, Account.Id account,
+      AccountGroup.UUID group) {
+    super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
+    this.ccFactory = ccFactory;
+    this.projectCache = projectCache;
+    this.userFactory = userFactory;
+    this.dbProvider = dbProvider;
+    this.label = label;
+    this.expVal = expVal;
+    this.account = account;
+    this.group = group;
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    Change c = object.change(dbProvider);
+    if (c == null) {
+      // The change has disappeared.
+      //
+      return false;
+    }
+    ProjectState project = projectCache.get(c.getDest().getParentKey());
+    if (project == null) {
+      // The project has disappeared.
+      //
+      return false;
+    }
+    LabelType labelType = type(project.getLabelTypes(), label);
+    boolean hasVote = false;
+    for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
+      if (labelType.matches(p)) {
+        hasVote = true;
+        if (match(c, p.getValue(), p.getAccountId(), labelType)) {
+          return true;
+        }
+      }
+    }
+
+    if (!hasVote && expVal == 0) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private static LabelType type(LabelTypes types, String toFind) {
+    if (types.byLabel(toFind) != null) {
+      return types.byLabel(toFind);
+    }
+
+    for (LabelType lt : types.getLabelTypes()) {
+      if (toFind.equalsIgnoreCase(lt.getName())) {
+        return lt;
+      }
+    }
+
+    for (LabelType lt : types.getLabelTypes()) {
+      if (toFind.equalsIgnoreCase(lt.getAbbreviation())) {
+        return lt;
+      }
+    }
+
+    return LabelType.withDefaultValues(toFind);
+  }
+
+  private boolean match(Change change, int value, Account.Id approver,
+      LabelType type) throws OrmException {
+    int psVal = value;
+    if (psVal == expVal) {
+      // Double check the value is still permitted for the user.
+      //
+      IdentifiedUser reviewer = userFactory.create(dbProvider, approver);
+      try {
+        ChangeControl cc = ccFactory.controlFor(change, reviewer);
+        if (!cc.isVisible(dbProvider.get())) {
+          // The user can't see the change anymore.
+          //
+          return false;
+        }
+        psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
+      } catch (NoSuchChangeException e) {
+        // The project has disappeared.
+        //
+        return false;
+      }
+
+      if (account != null && !account.equals(approver)) {
+        return false;
+      }
+
+      if (group != null && !reviewer.getEffectiveGroups().contains(group)) {
+        return false;
+      }
+
+      if (psVal == expVal) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1 + (group == null ? 0 : 1);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index 1d9a9a4..d5260a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -76,6 +76,6 @@
 
   @Override
   public int getCost() {
-    return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality());
+    return 0;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 639be517c..6832e4e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -18,15 +18,16 @@
 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.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class IsReviewedPredicate extends OperatorPredicate<ChangeData> {
+class IsReviewedPredicate extends IndexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
 
   IsReviewedPredicate(Provider<ReviewDb> dbProvider) {
-    super(ChangeQueryBuilder.FIELD_IS, "reviewed");
+    super(ChangeField.REVIEWED, "1");
     this.dbProvider = dbProvider;
   }
 
@@ -51,4 +52,9 @@
   public int getCost() {
     return 2;
   }
+
+  @Override
+  public String toString() {
+    return "is:reviewed";
+  }
 }
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 3fd9feb..9976bfd 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
@@ -14,28 +14,44 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.Lists;
+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.query.OperatorPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
-class IsStarredByPredicate extends OperatorPredicate<ChangeData> implements
+import java.util.List;
+import java.util.Set;
+
+class IsStarredByPredicate extends OrPredicate<ChangeData> implements
     ChangeDataSource {
   private static String describe(CurrentUser user) {
-    if (user instanceof IdentifiedUser) {
+    if (user.isIdentifiedUser()) {
       return ((IdentifiedUser) user).getAccountId().toString();
     }
     return user.toString();
   }
 
+  private static List<Predicate<ChangeData>> predicates(
+      Provider<ReviewDb> db,
+      Set<Change.Id> ids) {
+    List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
+    for (Change.Id id : ids) {
+      r.add(new LegacyChangeIdPredicate(db, id));
+    }
+    return r;
+  }
+
   private final Provider<ReviewDb> db;
   private final CurrentUser user;
 
   IsStarredByPredicate(Provider<ReviewDb> db, CurrentUser user) {
-    super(ChangeQueryBuilder.FIELD_STARREDBY, describe(user));
+    super(predicates(db, user.getStarredChanges()));
     this.db = db;
     this.user = user;
   }
@@ -63,6 +79,16 @@
 
   @Override
   public int getCost() {
-    return ChangeCosts.cost(ChangeCosts.IDS_MEMORY, getCardinality());
+    return 0;
+  }
+
+  @Override
+  public String toString() {
+    String val = describe(user);
+    if (val.indexOf(' ') < 0) {
+      return ChangeQueryBuilder.FIELD_STARREDBY + ":" + val;
+    } else {
+      return ChangeQueryBuilder.FIELD_STARREDBY + ":\"" + val + "\"";
+    }
   }
 }
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 b73465a..8992318 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
@@ -26,7 +26,7 @@
 
 class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
-    if (user instanceof IdentifiedUser) {
+    if (user.isIdentifiedUser()) {
       return ((IdentifiedUser) user).getAccountId().toString();
     }
     if (user instanceof SingleGroupUser) {
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 25cfae7..a6a344d 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
@@ -14,108 +14,103 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryBuilder;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gwtorm.server.OrmException;
 
-import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
-class IsWatchedByPredicate extends OperatorPredicate<ChangeData> {
+class IsWatchedByPredicate extends AndPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
-    if (user instanceof IdentifiedUser) {
+    if (user.isIdentifiedUser()) {
       return ((IdentifiedUser) user).getAccountId().toString();
     }
     return user.toString();
   }
 
-  private final ChangeQueryBuilder.Arguments args;
   private final CurrentUser user;
 
-  private Map<Project.NameKey, List<Predicate<ChangeData>>> rules;
-
-  IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, CurrentUser user) {
-    super(ChangeQueryBuilder.FIELD_WATCHEDBY, describe(user));
-    this.args = args;
+  IsWatchedByPredicate(ChangeQueryBuilder.Arguments args,
+      CurrentUser user,
+      boolean checkIsVisible) {
+    super(filters(args, user, checkIsVisible));
     this.user = user;
   }
 
-  @Override
-  public boolean match(final ChangeData cd) throws OrmException {
-    if (rules == null) {
-      ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
-      rules = new HashMap<Project.NameKey, List<Predicate<ChangeData>>>();
-      for (AccountProjectWatch w : user.getNotificationFilters()) {
-        List<Predicate<ChangeData>> list = rules.get(w.getProjectNameKey());
-        if (list == null) {
-          list = new ArrayList<Predicate<ChangeData>>(4);
-          rules.put(w.getProjectNameKey(), list);
-        }
-
-        Predicate<ChangeData> p = compile(builder, w);
-        if (p != null) {
-          list.add(p);
+  private static List<Predicate<ChangeData>> filters(
+      ChangeQueryBuilder.Arguments args,
+      CurrentUser user,
+      boolean checkIsVisible) {
+    List<Predicate<ChangeData>> r = Lists.newArrayList();
+    ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
+    for (AccountProjectWatch w : user.getNotificationFilters()) {
+      Predicate<ChangeData> f = null;
+      if (w.getFilter() != null) {
+        try {
+          f = builder.parse(w.getFilter());
+          if (QueryBuilder.find(f, IsWatchedByPredicate.class) != null) {
+            // If the query is going to infinite loop, assume it
+            // will never match and return null. Yes this test
+            // prevents you from having a filter that matches what
+            // another user is filtering on. :-)
+           continue;
+          }
+        } catch (QueryParseException e) {
+          continue;
         }
       }
-    }
 
-    if (rules.isEmpty()) {
-      return false;
-    }
+      Predicate<ChangeData> p;
+      if (w.getProjectNameKey().equals(args.allProjectsName)) {
+        p = null;
+      } else {
+        p = builder.project(w.getProjectNameKey().get());
+      }
 
-    Change change = cd.change(args.dbProvider);
-    if (change == null) {
-      return false;
-    }
-
-    Project.NameKey project = change.getDest().getParentKey();
-    List<Predicate<ChangeData>> list = rules.get(project);
-    if (list == null) {
-      list = rules.get(args.allProjectsName);
-    }
-    if (list != null) {
-      for (Predicate<ChangeData> p : list) {
-        if (p.match(cd)) {
-          return true;
-        }
+      if (p != null && f != null) {
+        @SuppressWarnings("unchecked")
+        Predicate<ChangeData> andPredicate = and(p, f);
+        r.add(andPredicate);
+      } else if (p != null) {
+        r.add(p);
+      } else if (f != null) {
+        r.add(f);
+      } else {
+        r.add(builder.status_open());
       }
     }
-    return false;
+    if (r.isEmpty()) {
+      return none();
+    } else if (checkIsVisible) {
+      return ImmutableList.of(or(r), builder.is_visible());
+    } else {
+      return ImmutableList.of(or(r));
+    }
   }
 
-  @SuppressWarnings("unchecked")
-  private Predicate<ChangeData> compile(ChangeQueryBuilder builder,
-      AccountProjectWatch w) {
-    Predicate<ChangeData> p = builder.is_visible();
-    if (w.getFilter() != null) {
-      try {
-        p = Predicate.and(builder.parse(w.getFilter()), p);
-        if (builder.find(p, IsWatchedByPredicate.class) != null) {
-          // If the query is going to infinite loop, assume it
-          // will never match and return null. Yes this test
-          // prevents you from having a filter that matches what
-          // another user is filtering on. :-)
-          //
-          return null;
-        }
-        p = args.rewriter.get().rewrite(p);
-      } catch (QueryParseException e) {
-        return null;
-      }
-    }
-    return p;
+  private static List<Predicate<ChangeData>> none() {
+    Predicate<ChangeData> any = any();
+    return ImmutableList.of(not(any));
   }
 
   @Override
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public String toString() {
+    String val = describe(user);
+    if (val.indexOf(' ') < 0) {
+      return ChangeQueryBuilder.FIELD_WATCHEDBY + ":" + val;
+    } else {
+      return ChangeQueryBuilder.FIELD_WATCHEDBY + ":\"" + val + "\"";
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index b0d02f8..b3ea6b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -14,84 +14,109 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
+import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 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.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
 import com.google.inject.Provider;
 
-import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-class LabelPredicate extends OperatorPredicate<ChangeData> {
+public class LabelPredicate extends OrPredicate<ChangeData> {
+  private static final int MAX_LABEL_VALUE = 4;
+
   private static enum Test {
-    EQ {
-      @Override
-      public boolean match(int psValue, int expValue) {
-        return psValue == expValue;
-      }
-    },
-    GT_EQ {
-      @Override
-      public boolean match(int psValue, int expValue) {
-        return psValue >= expValue;
-      }
-    },
-    LT_EQ {
-      @Override
-      public boolean match(int psValue, int expValue) {
-        return psValue <= expValue;
-      }
-    };
+    EQ, GT_EQ, LT_EQ;
 
-    abstract boolean match(int psValue, int expValue);
+    boolean isEq() {
+      return EQ.equals(this);
+    }
+
+    boolean isGtEq() {
+      return GT_EQ.equals(this);
+    }
+
+    static Test op(String op) {
+      if ("=".equals(op)) {
+        return EQ;
+
+      } else if (">=".equals(op)) {
+        return GT_EQ;
+
+      } else if ("<=".equals(op)) {
+        return LT_EQ;
+
+      } else {
+        throw new IllegalArgumentException("Unsupported operation " + op);
+      }
+    }
   }
 
-  private static LabelType type(LabelTypes types, String toFind) {
-    if (types.byLabel(toFind) != null) {
-      return types.byLabel(toFind);
-    }
+  private final String value;
 
-    for (LabelType lt : types.getLabelTypes()) {
-      if (toFind.equalsIgnoreCase(lt.getName())) {
-        return lt;
-      }
-    }
-
-    for (LabelType lt : types.getLabelTypes()) {
-      if (toFind.equalsIgnoreCase(lt.getAbbreviation())) {
-        return lt;
-      }
-    }
-
-    return LabelType.withDefaultValues(toFind);
+  LabelPredicate(ProjectCache projectCache,
+      ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String value, Set<Account.Id> accounts, AccountGroup.UUID group) {
+    super(predicates(projectCache, ccFactory, userFactory, dbProvider, value,
+        accounts, group));
+    this.value = value;
   }
 
-  private static Test op(String op) {
-    if ("=".equals(op)) {
-      return Test.EQ;
+  private static List<Predicate<ChangeData>> predicates(
+      ProjectCache projectCache, ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String value, Set<Account.Id> accounts, AccountGroup.UUID group) {
+    String label;
+    Test test;
+    int expVal;
+    Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
+    Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
+    if (m1.find()) {
+      label = value.substring(0, m1.start());
+      test = Test.op(m1.group(1));
+      expVal = value(m1.group(2));
 
-    } else if (">=".equals(op)) {
-      return Test.GT_EQ;
-
-    } else if ("<=".equals(op)) {
-      return Test.LT_EQ;
+    } else if (m2.find()) {
+      label = value.substring(0, m2.start());
+      test = Test.EQ;
+      expVal = value(m2.group(1));
 
     } else {
-      throw new IllegalArgumentException("Unsupported operation " + op);
+      label = value;
+      test = Test.EQ;
+      expVal = 1;
     }
+
+    List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
+    if (test.isEq()) {
+      if (expVal != 0) {
+        r.add(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+            dbProvider, label, expVal, accounts, group));
+      } else {
+        r.add(noLabelQuery(projectCache, ccFactory, userFactory,
+            dbProvider, label, accounts, group));
+      }
+    } else {
+      for (int i = test.isGtEq() ? expVal : neg(expVal); i <= MAX_LABEL_VALUE; i++) {
+        if (i != 0) {
+          r.add(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+              dbProvider, label, test.isGtEq() ? i : neg(i), accounts, group));
+        } else {
+          r.add(noLabelQuery(projectCache, ccFactory, userFactory,
+              dbProvider, label, accounts, group));
+        }
+      }
+    }
+    return r;
   }
 
   private static int value(String value) {
@@ -101,113 +126,45 @@
     return Integer.parseInt(value);
   }
 
-  private final ProjectCache projectCache;
-  private final ChangeControl.GenericFactory ccFactory;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final Provider<ReviewDb> dbProvider;
-  private final Test test;
-  private final String type;
-  private final int expVal;
+  private static int neg(int value) {
+    return -1 * value;
+  }
 
-  LabelPredicate(ProjectCache projectCache,
+  private static Predicate<ChangeData> noLabelQuery(ProjectCache projectCache,
       ChangeControl.GenericFactory ccFactory,
-      IdentifiedUser.GenericFactory userFactory,
-      Provider<ReviewDb> dbProvider,
-      String value) {
-    super(ChangeQueryBuilder.FIELD_LABEL, value);
-    this.ccFactory = ccFactory;
-    this.projectCache = projectCache;
-    this.userFactory = userFactory;
-    this.dbProvider = dbProvider;
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String label, Set<Account.Id> accounts, AccountGroup.UUID group) {
+    List<Predicate<ChangeData>> r =
+        Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
+    for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
+      r.add(not(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+          dbProvider, label, i, accounts, group)));
+      r.add(not(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+          dbProvider, label, neg(i), accounts, group)));
+    }
+    return and(r);
+  }
 
-    Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
-    Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
-    if (m1.find()) {
-      type = value.substring(0, m1.start());
-      test = op(m1.group(1));
-      expVal = value(m1.group(2));
-
-    } else if (m2.find()) {
-      type = value.substring(0, m2.start());
-      test = Test.EQ;
-      expVal = value(m2.group(1));
-
+  private static Predicate<ChangeData> equalsLabelPredicate(
+      ProjectCache projectCache, ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String label, int expVal, Set<Account.Id> accounts,
+      AccountGroup.UUID group) {
+    if (accounts == null || accounts.isEmpty()) {
+      return new EqualsLabelPredicate(projectCache, ccFactory, userFactory,
+          dbProvider, label, expVal, null, group);
     } else {
-      type = value;
-      test = Test.EQ;
-      expVal = 1;
+      List<Predicate<ChangeData>> r = Lists.newArrayList();
+      for (Account.Id a : accounts) {
+        r.add(new EqualsLabelPredicate(projectCache, ccFactory, userFactory,
+            dbProvider, label, expVal, a, group));
+      }
+      return or(r);
     }
   }
 
   @Override
-  public boolean match(final ChangeData object) throws OrmException {
-    final Change c = object.change(dbProvider);
-    if (c == null) {
-      // The change has disappeared.
-      //
-      return false;
-    }
-    final ProjectState project = projectCache.get(c.getDest().getParentKey());
-    if (project == null) {
-      // The project has disappeared.
-      //
-      return false;
-    }
-    final LabelType labelType = type(project.getLabelTypes(), type);
-    final Set<Account.Id> allApprovers = new HashSet<Account.Id>();
-    final Set<Account.Id> approversThatVotedInCategory = new HashSet<Account.Id>();
-    for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
-      allApprovers.add(p.getAccountId());
-      if (labelType.matches(p)) {
-        approversThatVotedInCategory.add(p.getAccountId());
-        if (match(c, p.getValue(), p.getAccountId(), labelType)) {
-          return true;
-        }
-      }
-    }
-
-    final Set<Account.Id> approversThatDidNotVoteInCategory = new HashSet<Account.Id>(allApprovers);
-    approversThatDidNotVoteInCategory.removeAll(approversThatVotedInCategory);
-    for (Account.Id a : approversThatDidNotVoteInCategory) {
-      if (match(c, 0, a, labelType)) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  private boolean match(final Change change, final int value,
-      final Account.Id approver, final LabelType type)
-      throws OrmException {
-    int psVal = value;
-    if (test.match(psVal, expVal)) {
-      // Double check the value is still permitted for the user.
-      //
-      try {
-        ChangeControl cc = ccFactory.controlFor(change, //
-            userFactory.create(dbProvider, approver));
-        if (!cc.isVisible(dbProvider.get())) {
-          // The user can't see the change anymore.
-          //
-          return false;
-        }
-        psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
-      } catch (NoSuchChangeException e) {
-        // The project has disappeared.
-        //
-        return false;
-      }
-
-      if (test.match(psVal, expVal)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public int getCost() {
-    return 2;
+  public String toString() {
+    return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index 4c47e73..48ae48f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -16,7 +16,8 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -24,13 +25,13 @@
 
 import java.util.Collections;
 
-class LegacyChangeIdPredicate extends OperatorPredicate<ChangeData> implements
+class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> implements
     ChangeDataSource {
   private final Provider<ReviewDb> db;
   private final Change.Id id;
 
   LegacyChangeIdPredicate(Provider<ReviewDb> db, Change.Id id) {
-    super(ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+    super(ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
     this.db = db;
     this.id = id;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyMessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyMessagePredicate.java
new file mode 100644
index 0000000..6b6d1e5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyMessagePredicate.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Predicate to match changes that contains specified text in commit messages
+ * body.
+ */
+public class LegacyMessagePredicate extends RevWalkPredicate {
+
+  private static final Logger log = LoggerFactory
+      .getLogger(LegacyMessagePredicate.class);
+
+  private final RevFilter rFilter;
+
+  public LegacyMessagePredicate(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager, String text) {
+    super(db, repoManager, ChangeQueryBuilder.FIELD_MESSAGE, text);
+    this.rFilter = MessageRevFilter.create(text);
+  }
+
+  @Override
+  public boolean match(Repository repo, RevWalk rw, Arguments args) {
+    try {
+      return rFilter.include(rw, rw.parseCommit(args.objectId));
+    } catch (MissingObjectException e) {
+      log.error(args.projectName.get() + "\" commit does not exist.", e);
+    } catch (IncorrectObjectTypeException e) {
+      log.error(args.projectName.get() + "\" revision is not a commit.", e);
+    } catch (IOException e) {
+      log.error(
+          "Could not search for commit message in \"" + args.projectName.get()
+              + "\" repository.", e);
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 0ea280d..64e6b10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -15,49 +15,43 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
-import org.eclipse.jgit.revwalk.filter.RevFilter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
 /**
  * Predicate to match changes that contains specified text in commit messages
  * body.
  */
-public class MessagePredicate extends RevWalkPredicate {
+class MessagePredicate extends IndexPredicate<ChangeData> {
+  private final Provider<ReviewDb> db;
+  private final ChangeIndex index;
 
-  private static final Logger log =
-      LoggerFactory.getLogger(MessagePredicate.class);
-
-  private final RevFilter rFilter;
-
-  public MessagePredicate(Provider<ReviewDb> db,
-      GitRepositoryManager repoManager, String text) {
-    super(db, repoManager, ChangeQueryBuilder.FIELD_MESSAGE, text);
-    this.rFilter = MessageRevFilter.create(text);
+  MessagePredicate(Provider<ReviewDb> db, ChangeIndex index, String value) {
+    super(ChangeField.COMMIT_MESSAGE, value);
+    this.db = db;
+    this.index = index;
   }
 
+  @SuppressWarnings("unchecked")
   @Override
-  public boolean match(Repository repo, RevWalk rw, Arguments args) {
+  public boolean match(ChangeData object) throws OrmException {
     try {
-      return rFilter.include(rw, rw.parseCommit(args.objectId));
-    } catch (MissingObjectException e) {
-      log.error(args.projectName.get() + "\" commit does not exist.", e);
-    } catch (IncorrectObjectTypeException e) {
-      log.error(args.projectName.get() + "\" revision is not a commit.", e);
-    } catch (IOException e) {
-      log.error("Could not search for commit message in \"" +
-          args.projectName.get() + "\" repository.", e);
+      for (ChangeData cData : index.getSource(
+          Predicate.and(new LegacyChangeIdPredicate(db, object.getId()), this), 1)
+          .read()) {
+        if (cData.getId().equals(object.getId())) {
+          return true;
+        }
+      }
+    } catch (QueryParseException e) {
+      throw new OrmException(e);
     }
+
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
index ec5195b..4f36777 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
@@ -25,10 +25,10 @@
 import java.util.Collection;
 import java.util.HashSet;
 
-class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
+public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
   private int cardinality = -1;
 
-  OrSource(final Collection<? extends Predicate<ChangeData>> that) {
+  public OrSource(Collection<? extends Predicate<ChangeData>> that) {
     super(that);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index 7a85ef6..10f2d35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -17,16 +17,17 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class OwnerPredicate extends OperatorPredicate<ChangeData> {
+class OwnerPredicate extends IndexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final Account.Id id;
 
   OwnerPredicate(Provider<ReviewDb> dbProvider, Account.Id id) {
-    super(ChangeQueryBuilder.FIELD_OWNER, id.toString());
+    super(ChangeField.OWNER, id.toString());
     this.dbProvider = dbProvider;
     this.id = id;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
index 2b3edf7..d9ff80c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
@@ -17,7 +17,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
-interface Paginated {
+public interface Paginated {
   int limit();
 
   ResultSet<ChangeData> restart(ChangeData last) throws OrmException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
new file mode 100644
index 0000000..3e794b7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.query.QueryParseException;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is used to extract comma separated values in a predicate.
+ * <p>
+ * If tags for the values are present (e.g. "branch=jb_2.3,vote=approved") then
+ * the args are placed in a map that maps tag to value (e.g., "branch" to "jb_2.3").
+ * If no tag is present (e.g. "jb_2.3,approved") then the args are placed into a
+ * positional list.  Args may be mixed so some may appear in the map and others
+ * in the positional list (e.g. "vote=approved,jb_2.3).
+ */
+public class PredicateArgs {
+  public List<String> positional;
+  public Map<String, String> keyValue;
+
+  /**
+   * Parses query arguments into {@link #keyValue} and/or {@link #positional}..
+   * <p>
+   * Labels for these arguments should be kept in ChangeQueryBuilder
+   * as {@code ARG_ID_{argument name}}.
+   *
+   * @param args arguments to be parsed
+   * @throws QueryParseException
+   */
+  PredicateArgs(String args) throws QueryParseException {
+    positional = Lists.newArrayList();
+    keyValue = Maps.newHashMap();
+
+    String[] splitArgs = args.split(",");
+
+    for (String arg : splitArgs) {
+      String[] splitKeyValue = arg.split("=");
+
+      if (splitKeyValue.length == 1) {
+        positional.add(splitKeyValue[0]);
+      } else if (splitKeyValue.length == 2) {
+        if (!keyValue.containsKey(splitKeyValue[0])) {
+          keyValue.put(splitKeyValue[0], splitKeyValue[1]);
+        } else {
+          throw new QueryParseException("Duplicate key " + splitKeyValue[0]);
+        }
+      } else {
+        throw new QueryParseException("invalid arg " + arg);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index cce2f2a..fe8d937 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -17,15 +17,16 @@
 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.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class ProjectPredicate extends OperatorPredicate<ChangeData> {
+class ProjectPredicate extends IndexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
 
   ProjectPredicate(Provider<ReviewDb> dbProvider, String id) {
-    super(ChangeQueryBuilder.FIELD_PROJECT, id);
+    super(ChangeField.PROJECT, id);
     this.dbProvider = dbProvider;
   }
 
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 a005940..4b6a5a6 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
@@ -20,13 +20,14 @@
 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.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Option;
 
@@ -40,6 +41,7 @@
 public class QueryChanges implements RestReadView<TopLevelResource> {
   private final ChangeJson json;
   private final QueryProcessor imp;
+  private final Provider<CurrentUser> user;
   private boolean reverse;
   private EnumSet<ListChangesOption> options;
 
@@ -79,16 +81,12 @@
   }
 
   @Inject
-  QueryChanges(ChangeJson json,
-      QueryProcessor qp,
-      SshInfo sshInfo,
-      ChangeControl.Factory cf) {
+  QueryChanges(ChangeJson json, QueryProcessor qp, Provider<CurrentUser> user) {
     this.json = json;
     this.imp = qp;
+    this.user = user;
 
     options = EnumSet.noneOf(ListChangesOption.class);
-    json.setSshInfo(sshInfo);
-    json.setChangeControlFactory(cf);
   }
 
   public void addQuery(String query) {
@@ -98,6 +96,10 @@
     queries.add(query);
   }
 
+  public String getQuery(int i) {
+    return queries.get(i);
+  }
+
   @Override
   public Object apply(TopLevelResource rsrc)
       throws BadRequestException, AuthException, OrmException {
@@ -130,21 +132,36 @@
       throw new QueryParseException("limit of 10 queries");
     }
 
+    IdentifiedUser self = null;
+    try {
+      if (user.get().isIdentifiedUser()) {
+        self = (IdentifiedUser) user.get();
+        self.asyncStarredChanges();
+      }
+      return query0();
+    } finally {
+      if (self != null) {
+        self.abortStarredChanges();
+      }
+    }
+  }
+
+  private List<List<ChangeInfo>> query0() throws OrmException,
+      QueryParseException {
     int cnt = queries.size();
     BitSet more = new BitSet(cnt);
-    List<List<ChangeData>> data = Lists.newArrayListWithCapacity(cnt);
+    List<List<ChangeData>> data = imp.queryChanges(queries);
     for (int n = 0; n < cnt; n++) {
-      String query = queries.get(n);
-      List<ChangeData> changes = imp.queryChanges(query);
+      List<ChangeData> changes = data.get(n);
       if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) {
         if (reverse) {
           changes = changes.subList(1, changes.size());
         } else {
           changes = changes.subList(0, imp.getLimit());
         }
+        data.set(n, changes);
         more.set(n, true);
       }
-      data.add(changes);
     }
 
     List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 06d84c1..2f3fe54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
-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.CurrentUser;
@@ -30,8 +31,10 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -52,7 +55,6 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
-import java.util.HashSet;
 import java.util.List;
 
 public class QueryProcessor {
@@ -96,7 +98,8 @@
   private final ChangeQueryRewriter queryRewriter;
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
-  private final ChangeControl.Factory changeControlFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final CurrentUser user;
   private final int maxLimit;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
@@ -121,13 +124,14 @@
       ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
       ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db,
       GitRepositoryManager repoManager,
-      ChangeControl.Factory changeControlFactory) {
+      ChangeControl.GenericFactory changeControlFactory) {
     this.eventFactory = eventFactory;
     this.queryBuilder = queryBuilder.create(currentUser);
     this.queryRewriter = queryRewriter;
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
+    this.user = currentUser;
     this.maxLimit = currentUser.getCapabilities()
       .getRange(GlobalCapability.QUERY_LIMIT)
       .getMax();
@@ -211,47 +215,70 @@
    * there are more than {@code limit} matches and suggest to its own caller
    * that the query could be retried with {@link #setSortkeyBefore(String)}.
    */
-  public List<ChangeData> queryChanges(final String queryString)
+  public List<ChangeData> queryChanges(String queryString)
+      throws OrmException, QueryParseException {
+    return queryChanges(ImmutableList.of(queryString)).get(0);
+  }
+
+  /**
+   * Query for changes that match the query string.
+   * <p>
+   * If a limit was specified using {@link #setLimit(int)} this method may
+   * return up to {@code limit + 1} results, allowing the caller to determine if
+   * there are more than {@code limit} matches and suggest to its own caller
+   * that the query could be retried with {@link #setSortkeyBefore(String)}.
+   */
+  public List<List<ChangeData>> queryChanges(List<String> queries)
       throws OrmException, QueryParseException {
     final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
-    Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
-    List<ChangeData> results = new ArrayList<ChangeData>();
-    HashSet<Change.Id> want = new HashSet<Change.Id>();
-    for (ChangeData d : ((ChangeDataSource) s).read()) {
-      if (d.hasChange()) {
-        // Checking visibleToMe here should be unnecessary, the
-        // query should have already performed it. But we don't
-        // want to trust the query rewriter that much yet.
-        //
-        if (visibleToMe.match(d)) {
-          results.add(d);
-        }
-      } else {
-        want.add(d.getId());
+    int cnt = queries.size();
+
+    // Parse and rewrite all queries.
+    List<Integer> limits = Lists.newArrayListWithCapacity(cnt);
+    List<ChangeDataSource> sources = Lists.newArrayListWithCapacity(cnt);
+    for (int i = 0; i < cnt; i++) {
+      Predicate<ChangeData> q = parseQuery(queries.get(i), visibleToMe);
+      Predicate<ChangeData> s = queryRewriter.rewrite(q);
+      if (!(s instanceof ChangeDataSource)) {
+        @SuppressWarnings("unchecked")
+        Predicate<ChangeData> o = Predicate.and(queryBuilder.status_open(), q);
+        q = o;
+        s = queryRewriter.rewrite(q);
       }
+      if (!(s instanceof ChangeDataSource)) {
+        throw new QueryParseException("invalid query: " + s);
+      }
+
+      // Don't trust QueryRewriter to have left the visible predicate.
+      AndSource a = new AndSource(db, ImmutableList.of(s, visibleToMe));
+      limits.add(limit(q));
+      sources.add(a);
     }
 
-    if (!want.isEmpty()) {
-      for (Change c : db.get().changes().get(want)) {
-        ChangeData d = new ChangeData(c);
-        if (visibleToMe.match(d)) {
-          results.add(d);
-        }
-      }
+    // Run each query asynchronously, if supported.
+    List<ResultSet<ChangeData>> matches = Lists.newArrayListWithCapacity(cnt);
+    for (ChangeDataSource s : sources) {
+      matches.add(s.read());
     }
+    sources = null;
 
-    Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
-    int limit = limit(s);
-    if (results.size() > maxLimit) {
-      moreResults = true;
+    List<List<ChangeData>> out = Lists.newArrayListWithCapacity(cnt);
+    for (int i = 0; i < cnt; i++) {
+      List<ChangeData> results = matches.get(i).toList();
+      Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
+      if (results.size() > maxLimit) {
+        moreResults = true;
+      }
+      int limit = limits.get(i);
+      if (limit < results.size()) {
+        results = results.subList(0, limit);
+      }
+      if (sortkeyAfter != null) {
+        Collections.reverse(results);
+      }
+      out.add(results);
     }
-    if (limit < results.size()) {
-      results = results.subList(0, limit);
-    }
-    if (sortkeyAfter != null) {
-      Collections.reverse(results);
-    }
-    return results;
+    return out;
   }
 
   public void query(String queryString) throws IOException {
@@ -268,13 +295,16 @@
 
       try {
         final QueryStatsAttribute stats = new QueryStatsAttribute();
-        stats.runTimeMilliseconds = System.currentTimeMillis();
+        stats.runTimeMilliseconds = TimeUtil.nowMs();
 
         List<ChangeData> results = queryChanges(queryString);
         ChangeAttribute c = null;
         for (ChangeData d : results) {
-          LabelTypes labelTypes = changeControlFactory.controlFor(d.getChange())
-              .getLabelTypes();
+          ChangeControl cc = d.changeControl();
+          if (cc == null || cc.getCurrentUser() != user) {
+            cc = changeControlFactory.controlFor(d.change(db), user);
+          }
+          LabelTypes labelTypes = cc.getLabelTypes();
           c = eventFactory.asChangeAttribute(d.getChange());
           eventFactory.extend(c, d.getChange());
           eventFactory.addTrackingIds(c, d.trackingIds(db));
@@ -282,7 +312,7 @@
           if (includeSubmitRecords) {
             PatchSet.Id psId = d.getChange().currentPatchSetId();
             PatchSet patchSet = db.get().patchSets().get(psId);
-            List<SubmitRecord> submitResult = d.changeControl().canSubmit( //
+            List<SubmitRecord> submitResult = cc.canSubmit( //
                 db.get(), patchSet, null, false, true, true);
             eventFactory.addSubmitRecords(c, submitResult);
           }
@@ -338,7 +368,7 @@
           stats.resumeSortKey = c.sortKey;
         }
         stats.runTimeMilliseconds =
-            System.currentTimeMillis() - stats.runTimeMilliseconds;
+            TimeUtil.nowMs() - stats.runTimeMilliseconds;
         show(stats);
       } catch (OrmException err) {
         log.error("Cannot execute query: " + queryString, err);
@@ -371,16 +401,17 @@
   }
 
   private int limit(Predicate<ChangeData> s) {
-    int n = queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+    int n = ChangeQueryBuilder.hasLimit(s)
+        ? ChangeQueryBuilder.getLimit(s)
+        : maxLimit;
     return limit > 0 ? Math.min(n, limit) + 1 : n + 1;
   }
 
   @SuppressWarnings("unchecked")
-  private Predicate<ChangeData> compileQuery(String queryString,
+  private Predicate<ChangeData> parseQuery(String queryString,
       final Predicate<ChangeData> visibleToMe) throws QueryParseException {
-
     Predicate<ChangeData> q = queryBuilder.parse(queryString);
-    if (!queryBuilder.hasSortKey(q)) {
+    if (!ChangeQueryBuilder.hasSortKey(q)) {
       if (sortkeyBefore != null) {
         q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
       } else if (sortkeyAfter != null) {
@@ -389,20 +420,9 @@
         q = Predicate.and(q, queryBuilder.sortkey_before("z"));
       }
     }
-    q = Predicate.and(q,
+    return Predicate.and(q,
         queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
         visibleToMe);
-
-    Predicate<ChangeData> s = queryRewriter.rewrite(q);
-    if (!(s instanceof ChangeDataSource)) {
-      s = queryRewriter.rewrite(Predicate.and(queryBuilder.status_open(), q));
-    }
-
-    if (!(s instanceof ChangeDataSource)) {
-      throw new QueryParseException("invalid query: " + s);
-    }
-
-    return s;
   }
 
   private void show(Object data) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
index 4811f9a..df0150f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -16,15 +16,16 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class RefPredicate extends OperatorPredicate<ChangeData> {
+class RefPredicate extends IndexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
 
   RefPredicate(Provider<ReviewDb> dbProvider, String ref) {
-    super(ChangeQueryBuilder.FIELD_REF, ref);
+    super(ChangeField.REF, ref);
     this.dbProvider = dbProvider;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
deleted file mode 100644
index 6704a10..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
-
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
-class RegexBranchPredicate extends OperatorPredicate<ChangeData> {
-  private final Provider<ReviewDb> dbProvider;
-  private final RunAutomaton pattern;
-
-  RegexBranchPredicate(Provider<ReviewDb> dbProvider, String re) {
-    super(ChangeQueryBuilder.FIELD_BRANCH, re);
-
-    if (re.startsWith("^")) {
-      re = re.substring(1);
-    }
-
-    if (re.endsWith("$") && !re.endsWith("\\$")) {
-      re = re.substring(0, re.length() - 1);
-    }
-
-    this.dbProvider = dbProvider;
-    this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
-  }
-
-  @Override
-  public boolean match(final ChangeData object) throws OrmException {
-    Change change = object.change(dbProvider);
-    if (change == null) {
-      return false;
-    }
-    return change.getDest().get().startsWith(Branch.R_HEADS)
-        && pattern.run(change.getDest().getShortName());
-  }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
index 11856e4..1ce9139 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
@@ -24,9 +25,10 @@
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 
-class RegexFilePredicate extends OperatorPredicate<ChangeData> {
+class RegexFilePredicate extends RegexPredicate<ChangeData> {
   private final Provider<ReviewDb> db;
   private final PatchListCache cache;
   private final RunAutomaton pattern;
@@ -37,7 +39,7 @@
   private final boolean prefixOnly;
 
   RegexFilePredicate(Provider<ReviewDb> db, PatchListCache plc, String re) {
-    super(ChangeQueryBuilder.FIELD_FILE, re);
+    super(ChangeField.FILE, re);
     this.db = db;
     this.cache = plc;
 
@@ -67,7 +69,7 @@
 
   @Override
   public boolean match(ChangeData object) throws OrmException {
-    String[] files = object.currentFilePaths(db, cache);
+    List<String> files = object.currentFilePaths(db, cache);
     if (files != null) {
       int begin, end;
 
@@ -76,7 +78,7 @@
         end = find(files, prefixEnd);
       } else {
         begin = 0;
-        end = files.length;
+        end = files.size();
       }
 
       if (prefixOnly) {
@@ -84,7 +86,7 @@
       }
 
       while (begin < end) {
-        if (pattern.run(files[begin++])) {
+        if (pattern.run(files.get(begin++))) {
           return true;
         }
       }
@@ -100,8 +102,8 @@
     }
   }
 
-  private static int find(String[] files, String p) {
-    int r = Arrays.binarySearch(files, p);
+  private static int find(List<String> files, String p) {
+    int r = Collections.binarySearch(files, p);
     return r < 0 ? -(r + 1) : r;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index b8911a4..b6c724d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -17,19 +17,20 @@
 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.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexProjectPredicate extends OperatorPredicate<ChangeData> {
+class RegexProjectPredicate extends RegexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final RunAutomaton pattern;
 
   RegexProjectPredicate(Provider<ReviewDb> dbProvider, String re) {
-    super(ChangeQueryBuilder.FIELD_PROJECT, re);
+    super(ChangeField.PROJECT, re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index 1480de6..22fb49b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -16,19 +16,20 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexRefPredicate extends OperatorPredicate<ChangeData> {
+class RegexRefPredicate extends RegexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final RunAutomaton pattern;
 
   RegexRefPredicate(Provider<ReviewDb> dbProvider, String re) {
-    super(ChangeQueryBuilder.FIELD_REF, re);
+    super(ChangeField.REF, re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 03814f8..51b9c48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -16,19 +16,20 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexTopicPredicate extends OperatorPredicate<ChangeData> {
+class RegexTopicPredicate extends RegexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final RunAutomaton pattern;
 
   RegexTopicPredicate(Provider<ReviewDb> dbProvider, String re) {
-    super(ChangeQueryBuilder.FIELD_TOPIC, re);
+    super(ChangeField.TOPIC, re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 8e910df..9e9d8bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -17,16 +17,17 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class ReviewerPredicate extends OperatorPredicate<ChangeData> {
+class ReviewerPredicate extends IndexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final Account.Id id;
 
   ReviewerPredicate(Provider<ReviewDb> dbProvider, Account.Id id) {
-    super(ChangeQueryBuilder.FIELD_REVIEWER, id.toString());
+    super(ChangeField.REVIEWER, id.toString());
     this.dbProvider = dbProvider;
     this.id = id;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
index e7668c6..c612945 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
@@ -14,17 +14,52 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.index.ChangeField.SORTKEY;
+
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-abstract class SortKeyPredicate extends OperatorPredicate<ChangeData> {
+public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
+  @SuppressWarnings("deprecation")
+  private static long parseSortKey(Schema<ChangeData> schema, String value) {
+    FieldDef<ChangeData, ?> field = schema.getFields().get(SORTKEY.getName());
+    if (field == SORTKEY) {
+      return ChangeUtil.parseSortKey(value);
+    } else {
+      return ChangeField.legacyParseSortKey(value);
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  private static FieldDef<ChangeData, ?> sortkeyField(Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_SORTKEY;
+    }
+    FieldDef<ChangeData, ?> f = schema.getFields().get(SORTKEY.getName());
+    if (f != null) {
+      return f;
+    }
+    return checkNotNull(
+        schema.getFields().get(ChangeField.LEGACY_SORTKEY.getName()),
+        "schema missing sortkey field, found: %s", schema.getFields().keySet());
+  }
+
+  protected final Schema<ChangeData> schema;
   protected final Provider<ReviewDb> dbProvider;
 
-  SortKeyPredicate(Provider<ReviewDb> dbProvider, String name, String value) {
-    super(name, value);
+  SortKeyPredicate(Schema<ChangeData> schema, Provider<ReviewDb> dbProvider,
+      String name, String value) {
+    super(sortkeyField(schema), name, value);
+    this.schema = schema;
     this.dbProvider = dbProvider;
   }
 
@@ -33,9 +68,24 @@
     return 1;
   }
 
-  static class Before extends SortKeyPredicate {
-    Before(Provider<ReviewDb> dbProvider, String value) {
-      super(dbProvider, "sortkey_before", value);
+  public abstract long getMinValue(Schema<ChangeData> schema);
+  public abstract long getMaxValue(Schema<ChangeData> schema);
+  public abstract SortKeyPredicate copy(String newValue);
+
+  public static class Before extends SortKeyPredicate {
+    Before(@Nullable Schema<ChangeData> schema, Provider<ReviewDb> dbProvider,
+        String value) {
+      super(schema, dbProvider, "sortkey_before", value);
+    }
+
+    @Override
+    public long getMinValue(Schema<ChangeData> schema) {
+      return 0;
+    }
+
+    @Override
+    public long getMaxValue(Schema<ChangeData> schema) {
+      return parseSortKey(schema, getValue());
     }
 
     @Override
@@ -43,11 +93,27 @@
       Change change = cd.change(dbProvider);
       return change != null && change.getSortKey().compareTo(getValue()) < 0;
     }
+
+    @Override
+    public Before copy(String newValue) {
+      return new Before(schema, dbProvider, newValue);
+    }
   }
 
-  static class After extends SortKeyPredicate {
-    After(Provider<ReviewDb> dbProvider, String value) {
-      super(dbProvider, "sortkey_after", value);
+  public static class After extends SortKeyPredicate {
+    After(@Nullable Schema<ChangeData> schema, Provider<ReviewDb> dbProvider,
+        String value) {
+      super(schema, dbProvider, "sortkey_after", value);
+    }
+
+    @Override
+    public long getMinValue(Schema<ChangeData> schema) {
+      return parseSortKey(schema, getValue());
+    }
+
+    @Override
+    public long getMaxValue(Schema<ChangeData> schema) {
+      return Long.MAX_VALUE;
     }
 
     @Override
@@ -55,5 +121,10 @@
       Change change = cd.change(dbProvider);
       return change != null && change.getSortKey().compareTo(getValue()) > 0;
     }
+
+    @Override
+    public After copy(String newValue) {
+      return new After(schema, dbProvider, newValue);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java
new file mode 100644
index 0000000..bcc4859
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java
@@ -0,0 +1,658 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.gerrit.server.query.RewritePredicate;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.LimitPredicate;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import java.util.Collection;
+
+public class SqlRewriterImpl extends BasicChangeRewrites
+    implements ChangeQueryRewriter {
+  private static final QueryRewriter.Definition<ChangeData, SqlRewriterImpl> mydef =
+      new QueryRewriter.Definition<ChangeData, SqlRewriterImpl>(
+          SqlRewriterImpl.class, BUILDER);
+
+  @Inject
+  @VisibleForTesting
+  public SqlRewriterImpl(Provider<ReviewDb> dbProvider) {
+    super(mydef, dbProvider, null);
+  }
+
+  @Override
+  public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
+    return hasSource(l) ? new AndSource(dbProvider, l) : super.and(l);
+  }
+
+  @Override
+  public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
+    return hasSource(l) ? new OrSource(l) : super.or(l);
+  }
+
+  @Rewrite("status:open P=(project:*) B=(ref:*)")
+  public Predicate<ChangeData> r05_byBranchOpen(
+      @Named("P") final ProjectPredicate p,
+      @Named("B") final RefPredicate b) {
+    return new ChangeSource(500) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a)
+          throws OrmException {
+        return a.byBranchOpenAll(
+            new Branch.NameKey(p.getValueKey(), b.getValue()));
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen()
+            && p.match(cd)
+            && b.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r05_byBranchMergedPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("B") final RefPredicate b,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byBranchClosedPrev(Change.Status.MERGED.getCode(), //
+            new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && b.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r05_byBranchMergedNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("B") final RefPredicate b,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byBranchClosedNext(Change.Status.MERGED.getCode(), //
+            new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && b.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectOpenPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(500, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectOpenPrev(p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() //
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectOpenNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(500, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectOpenNext(p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() //
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectMergedPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectMergedNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedNext(Change.Status.MERGED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectAbandonedPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r10_byProjectAbandonedNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), //
+            p.getValueKey(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && p.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byOpenPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allOpenPrev(key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byOpenNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allOpenNext(key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byMergedPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      {
+        init("r20_byMergedPrev", s, l);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byMergedNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      {
+        init("r20_byMergedNext", s, l);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byAbandonedPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      {
+        init("r20_byAbandonedPrev", s, l);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byAbandonedNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+      {
+        init("r20_byAbandonedNext", s, l);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+            && s.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byClosedPrev(
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r20_byClosedNext(
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:open O=(owner:*)")
+  public Predicate<ChangeData> r25_byOwnerOpen(
+      @Named("O") final OwnerPredicate o) {
+    return new ChangeSource(50) {
+      {
+        init("r25_byOwnerOpen", o);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.byOwnerOpen(o.getAccountId());
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen() && o.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:closed O=(owner:*)")
+  public Predicate<ChangeData> r25_byOwnerClosed(
+      @Named("O") final OwnerPredicate o) {
+    return new ChangeSource(5000) {
+      {
+        init("r25_byOwnerClosed", o);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.byOwnerClosedAll(o.getAccountId());
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isClosed() && o.match(cd);
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("O=(owner:*)")
+  public Predicate<ChangeData> r26_byOwner(@Named("O") OwnerPredicate o) {
+    return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:open R=(reviewer:*)")
+  public Predicate<ChangeData> r30_byReviewerOpen(
+      @Named("R") final ReviewerPredicate r) {
+    return new Source() {
+      {
+        init("r30_byReviewerOpen", r);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      public ResultSet<ChangeData> read() throws OrmException {
+        return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+            .patchSetApprovals().openByUser(r.getAccountId()));
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        Change change = cd.change(dbProvider);
+        return change != null && change.getStatus().isOpen() && r.match(cd);
+      }
+
+      @Override
+      public int getCardinality() {
+        return 50;
+      }
+
+      @Override
+      public int getCost() {
+        return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("status:closed R=(reviewer:*)")
+  public Predicate<ChangeData> r30_byReviewerClosed(
+      @Named("R") final ReviewerPredicate r) {
+    return new Source() {
+      {
+        init("r30_byReviewerClosed", r);
+      }
+
+      @SuppressWarnings("deprecation")
+      @Override
+      public ResultSet<ChangeData> read() throws OrmException {
+        return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+            .patchSetApprovals().closedByUserAll(r.getAccountId()));
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        Change change = cd.change(dbProvider);
+        return change != null && change.getStatus().isClosed() && r.match(cd);
+      }
+
+      @Override
+      public int getCardinality() {
+        return 5000;
+      }
+
+      @Override
+      public int getCost() {
+        return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rewrite("R=(reviewer:*)")
+  public Predicate<ChangeData> r31_byReviewer(
+      @Named("R") final ReviewerPredicate r) {
+    return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r));
+  }
+
+  @Rewrite("status:closed")
+  public Predicate<ChangeData> r99_allClosed() {
+    return r20_byClosedNext(
+        new SortKeyPredicate.Before(null, dbProvider, "z"),
+        new LimitPredicate(Integer.MAX_VALUE));
+  }
+
+  @Rewrite("status:submitted")
+  public Predicate<ChangeData> r99_allSubmitted() {
+    return new ChangeSource(50) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.allSubmitted();
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED;
+      }
+    };
+  }
+
+  @Rewrite("P=(project:*)")
+  public Predicate<ChangeData> r99_byProject(
+      @Named("P") final ProjectPredicate p) {
+    return new ChangeSource(1000000) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+        return a.byProject(p.getValueKey());
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return p.match(cd);
+      }
+    };
+  }
+
+  private static boolean hasSource(Collection<? extends Predicate<ChangeData>> l) {
+    for (Predicate<ChangeData> p : l) {
+      if (p instanceof ChangeDataSource) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private abstract static class Source extends RewritePredicate<ChangeData>
+      implements ChangeDataSource {
+    @Override
+    public boolean hasChange() {
+      return false;
+    }
+  }
+
+  private abstract class ChangeSource extends Source {
+    private final int cardinality;
+
+    ChangeSource(int card) {
+      this.cardinality = card;
+    }
+
+    abstract ResultSet<Change> scan(ChangeAccess a) throws OrmException;
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      return ChangeDataResultSet.change(scan(dbProvider.get().changes()));
+    }
+
+    @Override
+    public boolean hasChange() {
+      return true;
+    }
+
+    @Override
+    public int getCardinality() {
+      return cardinality;
+    }
+
+    @Override
+    public int getCost() {
+      return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
+    }
+  }
+
+  private abstract class PaginatedSource extends ChangeSource implements
+      Paginated {
+    private final String startKey;
+    private final int limit;
+
+    PaginatedSource(int card, String start, int lim) {
+      super(card);
+      this.startKey = start;
+      this.limit = lim;
+    }
+
+    @Override
+    public int limit() {
+      return limit;
+    }
+
+    @Override
+    ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+      return scan(a, startKey, limit);
+    }
+
+    @Override
+    public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
+      return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
+          last.change(dbProvider).getSortKey(), //
+          limit));
+    }
+
+    abstract ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+        throws OrmException;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
index 8d58376..9393fe1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
@@ -16,15 +16,16 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class TopicPredicate extends OperatorPredicate<ChangeData> {
+class TopicPredicate extends IndexPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
 
   TopicPredicate(Provider<ReviewDb> dbProvider, String topic) {
-    super(ChangeQueryBuilder.FIELD_TOPIC, topic);
+    super(ChangeField.TOPIC, topic);
     this.dbProvider = dbProvider;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index e022f86..89e5ba9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -17,7 +17,8 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -26,12 +27,12 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 
-class TrackingIdPredicate extends OperatorPredicate<ChangeData> implements
+class TrackingIdPredicate extends IndexPredicate<ChangeData> implements
     ChangeDataSource {
   private final Provider<ReviewDb> db;
 
   TrackingIdPredicate(Provider<ReviewDb> db, String trackingId) {
-    super(ChangeQueryBuilder.FIELD_TR, trackingId);
+    super(ChangeField.TR, trackingId);
     this.db = db;
   }
 
@@ -45,6 +46,7 @@
     return false;
   }
 
+  @SuppressWarnings("deprecation")
   @Override
   public ResultSet<ChangeData> read() throws OrmException {
     HashSet<Change.Id> ids = new HashSet<Change.Id>();
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 4066ad3..9aeda09 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
@@ -24,6 +24,7 @@
     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);
+    bind(DataSourceType.class).annotatedWith(Names.named("oracle")).to(Oracle.class);
     bind(DataSourceType.class).annotatedWith(Names.named("postgresql")).to(PostgreSQL.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 6c9ca59d..c96ed42 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -41,6 +41,8 @@
 @Singleton
 public class DataSourceProvider implements Provider<DataSource>,
     LifecycleListener {
+  public static final int DEFAULT_POOL_LIMIT = 8;
+
   private final SitePaths site;
   private final Config cfg;
   private final Context ctx;
@@ -118,7 +120,7 @@
       if (password != null && !password.isEmpty()) {
         ds.setPassword(password);
       }
-      ds.setMaxActive(cfg.getInt("database", "poollimit", 8));
+      ds.setMaxActive(cfg.getInt("database", "poollimit", DEFAULT_POOL_LIMIT));
       ds.setMinIdle(cfg.getInt("database", "poolminidle", 4));
       ds.setMaxIdle(cfg.getInt("database", "poolmaxidle", 4));
       ds.setMaxWait(ConfigUtil.getTimeUnit(cfg, "database", null,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
new file mode 100644
index 0000000..bb4c477
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
@@ -0,0 +1,46 @@
+// 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.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+public class Oracle extends BaseDataSourceType {
+  private Config cfg;
+
+  @Inject
+  public Oracle(@GerritServerConfig final Config cfg) {
+    super("oracle.jdbc.driver.OracleDriver");
+    this.cfg = cfg;
+  }
+
+  @Override
+  public String getUrl() {
+    final StringBuilder b = new StringBuilder();
+    final ConfigSection dbc = new ConfigSection(cfg, "database");
+    b.append("jdbc:oracle:thin:@");
+    b.append(hostname(dbc.optional("hostname")));
+    b.append(port(dbc.optional("port")));
+    b.append(":");
+    b.append(dbc.required("instance"));
+    return b.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index 305fb84..0cea4bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -61,7 +61,7 @@
 
       } else {
         try {
-          u.check(ui, version, db, true);
+          u.check(ui, version, db);
         } catch (SQLException e) {
           throw new OrmException("Cannot upgrade schema", e);
         }
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 53930ac..2882d00 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcExecutor;
@@ -25,14 +26,13 @@
 
 import java.sql.SQLException;
 import java.sql.Statement;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_79> C = Schema_79.class;
+  public static final Class<Schema_84> C = Schema_84.class;
 
   public static class Module extends AbstractModule {
     @Override
@@ -68,59 +68,83 @@
     return versionNbr;
   }
 
-  public final void check(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db, boolean toTargetVersion)
+  public final void check(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
       throws OrmException, SQLException {
     if (curr.versionNbr == versionNbr) {
       // Nothing to do, we are at the correct schema.
-      //
+    } else if (curr.versionNbr > versionNbr) {
+      throw new OrmException("Cannot downgrade database schema from version "
+          + curr.versionNbr + " to " + versionNbr + ".");
     } else {
-      upgradeFrom(ui, curr, db, toTargetVersion);
+      upgradeFrom(ui, curr, db);
     }
   }
 
   /** Runs check on the prior schema version, and then upgrades. */
-  protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db, boolean toTargetVersion)
+  private void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
       throws OrmException, SQLException {
-    final JdbcSchema s = (JdbcSchema) db;
+    List<SchemaVersion> pending = pending(curr.versionNbr);
+    updateSchema(pending, ui, db);
+    migrateData(pending, ui, curr, db);
 
-    if (curr.versionNbr > versionNbr) {
-      throw new OrmException("Cannot downgrade database schema from version " + curr.versionNbr
-          + " to " + versionNbr + ".");
-    }
-
-    prior.get().check(ui, curr, db, false);
-
-    ui.message("Upgrading database schema from version " + curr.versionNbr
-        + " to " + versionNbr + " ...");
-
-    preUpdateSchema(db);
-    final JdbcExecutor e = new JdbcExecutor(s);
+    JdbcSchema s = (JdbcSchema) db;
+    JdbcExecutor e = new JdbcExecutor(s);
     try {
-      s.updateSchema(e);
-      migrateData(db, ui);
-
-      if (toTargetVersion) {
-        final List<String> pruneList = new ArrayList<String>();
-        s.pruneSchema(new StatementExecutor() {
-          public void execute(String sql) {
-            pruneList.add(sql);
-          }
-        });
-
-        if (!pruneList.isEmpty()) {
-          ui.pruneSchema(e, pruneList);
+      final List<String> pruneList = Lists.newArrayList();
+      s.pruneSchema(new StatementExecutor() {
+        public void execute(String sql) {
+          pruneList.add(sql);
         }
+      });
+
+      if (!pruneList.isEmpty()) {
+        ui.pruneSchema(e, pruneList);
       }
     } finally {
       e.close();
     }
-    finish(curr, db);
+  }
+
+  private List<SchemaVersion> pending(int curr) {
+    List<SchemaVersion> r = Lists.newArrayListWithCapacity(versionNbr - curr);
+    for (SchemaVersion v = this; curr < v.getVersionNbr(); v = v.prior.get()) {
+      r.add(v);
+    }
+    Collections.reverse(r);
+    return r;
+  }
+
+  private void updateSchema(List<SchemaVersion> pending, UpdateUI ui,
+      ReviewDb db) throws OrmException, SQLException {
+    for (SchemaVersion v : pending) {
+      ui.message(String.format("Upgrading schema to %d ...", v.getVersionNbr()));
+      v.preUpdateSchema(db);
+    }
+
+    JdbcSchema s = (JdbcSchema) db;
+    JdbcExecutor e = new JdbcExecutor(s);
+    try {
+      s.updateSchema(e);
+    } finally {
+      e.close();
+    }
   }
 
   /** Invoke before updateSchema adds new columns/tables. */
   protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
   }
 
+  private void migrateData(List<SchemaVersion> pending, UpdateUI ui,
+      CurrentSchemaVersion curr, ReviewDb db) throws OrmException, SQLException {
+    for (SchemaVersion v : pending) {
+      ui.message(String.format(
+          "Migrating data to schema %d ...",
+          v.getVersionNbr()));
+      v.migrateData(db, ui);
+      v.finish(curr, db);
+    }
+  }
+
   /**
    * Invoked between updateSchema (adds new columns/tables) and pruneSchema
    * (removes deleted columns/tables).
@@ -135,6 +159,18 @@
     db.schemaVersion().update(Collections.singleton(curr));
   }
 
+  /** Rename an existing table. */
+  protected void renameTable(ReviewDb db, String from, String to)
+      throws OrmException {
+    final JdbcSchema s = (JdbcSchema) db;
+    final JdbcExecutor e = new JdbcExecutor(s);
+    try {
+      s.renameTable(e, from, to);
+    } finally {
+      e.close();
+    }
+  }
+
   /** Rename an existing column. */
   protected void renameColumn(ReviewDb db, String table, String from, String to)
       throws OrmException {
@@ -147,7 +183,7 @@
     }
   }
 
-  /** Execute a SQL statement. */
+  /** Execute an SQL statement. */
   protected void execute(ReviewDb db, String sql) throws SQLException {
     Statement s = ((JdbcSchema) db).getConnection().createStatement();
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
index 12a22f9..76dbdf5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -30,12 +27,4 @@
       }
     });
   }
-
-  @Override
-  protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr,
-      ReviewDb db, boolean toTargetVersion) throws OrmException {
-    throw new OrmException("Cannot upgrade from schema " + curr.versionNbr
-        + "; manually run init from Gerrit Code Review 2.1.7"
-        + " and restart this version to continue.");
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
index 9032bb0..006c759 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -30,10 +31,10 @@
 import java.util.Collections;
 
 public class Schema_55 extends SchemaVersion {
-  private final LocalDiskRepositoryManager mgr;
+  private final GitRepositoryManager mgr;
 
   @Inject
-  Schema_55(Provider<Schema_54> prior, LocalDiskRepositoryManager mgr) {
+  Schema_55(Provider<Schema_54> prior, GitRepositoryManager mgr) {
     super(prior);
     this.mgr = mgr;
   }
@@ -46,7 +47,7 @@
     if ("-- All Projects --".equals(oldName)) {
       ui.message("Renaming \"" + oldName + "\" to \"" + newName + "\"");
 
-      File base = mgr.getBasePath();
+      File base = ((LocalDiskRepositoryManager) mgr).getBasePath();
       File oldDir = FileKey.resolve(new File(base, oldName), FS.DETECTED);
       File newDir = new File(base, newName + Constants.DOT_GIT_EXT);
       if (!oldDir.renameTo(newDir)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java
index 2409b12e..3ba77ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java
@@ -17,13 +17,12 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 
@@ -33,12 +32,12 @@
 import java.util.Set;
 
 public class Schema_56 extends SchemaVersion {
-  private final LocalDiskRepositoryManager mgr;
+  private final GitRepositoryManager mgr;
   private final Set<String> keysOne;
   private final Set<String> keysTwo;
 
   @Inject
-  Schema_56(Provider<Schema_55> prior, LocalDiskRepositoryManager mgr) {
+  Schema_56(Provider<Schema_55> prior, GitRepositoryManager mgr) {
     super(prior);
     this.mgr = mgr;
 
@@ -56,12 +55,20 @@
       Repository git;
       try {
         git = mgr.openRepository(name);
-      } catch (RepositoryNotFoundException e) {
+      } catch (IOException e) {
         ui.message("warning: Cannot open " + name.get());
         continue;
       }
       try {
-        Map<String, Ref> all = git.getAllRefs();
+        Map<String, Ref> all;
+        try {
+          all = git.getRefDatabase().getRefs(RefDatabase.ALL);
+        } catch (IOException e) {
+          ui.message("warning: " + name.get() + ": Cannot read refs: "
+              + e.getMessage());
+          e.printStackTrace();
+          continue;
+        }
         if (all.keySet().equals(keysOne) || all.keySet().equals(keysTwo)) {
           try {
             RefUpdate update = git.updateRef(Constants.HEAD);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index bf488e3..4b4f5456 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -43,17 +43,18 @@
 
 import java.io.IOException;
 import java.sql.PreparedStatement;
+import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.Collections;
 
 public class Schema_57 extends SchemaVersion {
   private final SitePaths site;
-  private final LocalDiskRepositoryManager mgr;
+  private final GitRepositoryManager mgr;
   private final PersonIdent serverUser;
 
   @Inject
   Schema_57(Provider<Schema_56> prior, SitePaths site,
-      LocalDiskRepositoryManager mgr, @GerritPersonIdent PersonIdent serverUser) {
+      GitRepositoryManager mgr, @GerritPersonIdent PersonIdent serverUser) {
     super(prior);
     this.site = site;
     this.mgr = mgr;
@@ -101,7 +102,17 @@
 
         // Prepare the account_group_includes query
         PreparedStatement stmt = ((JdbcSchema) db).getConnection().
-            prepareStatement("SELECT * FROM account_group_includes WHERE group_id = ?");
+            prepareStatement("SELECT COUNT(1) FROM account_group_includes WHERE group_id = ?");
+        boolean isAccountGroupEmpty = false;
+        try {
+          stmt.setInt(1, sc.batchUsersGroupId.get());
+          ResultSet rs = stmt.executeQuery();
+          if (rs.next()) {
+            isAccountGroupEmpty = rs.getInt(1) == 0;
+          }
+        } finally {
+          stmt.close();
+        }
 
         for (String name : createGroupList) {
           AccountGroup.NameKey key = new AccountGroup.NameKey(name);
@@ -125,10 +136,10 @@
         }
 
         AccountGroup batch = db.accountGroups().get(sc.batchUsersGroupId);
-        stmt.setInt(1, sc.batchUsersGroupId.get());
+
         if (batch != null
             && db.accountGroupMembers().byGroup(sc.batchUsersGroupId).toList().isEmpty()
-            &&  stmt.executeQuery().first() != false) {
+            && !isAccountGroupEmpty) {
           // If the batch user group is not used, delete it.
           //
           db.accountGroups().delete(Collections.singleton(batch));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
index 1cdf25c..624a51b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -348,7 +349,7 @@
           "       reviewed_on, review_comments " +
           "FROM account_agreements WHERE status = 'V'");
       try {
-        long minTime = System.currentTimeMillis();
+        long minTime = TimeUtil.nowMs();
         while (rs.next()) {
           Account.Id accountId = new Account.Id(rs.getInt(1));
           Account.Id reviewerId = new Account.Id(rs.getInt(4));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
index 94f5d2c..9ee2c6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
@@ -14,33 +14,13 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.sql.SQLException;
-import java.sql.Statement;
-
 public class Schema_66 extends SchemaVersion {
 
   @Inject
   Schema_66(Provider<Schema_65> prior) {
     super(prior);
   }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui)
-      throws OrmException, SQLException {
-    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.executeUpdate("UPDATE accounts SET reverse_patch_set_order = 'Y' "+
-                         "WHERE display_patch_sets_in_reverse_order = 'Y'");
-      stmt.executeUpdate("UPDATE accounts SET show_username_in_review_category = 'Y' " +
-                         "WHERE display_person_name_in_review_category = 'Y'");
-    } finally {
-      stmt.close();
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
index ca012d1..4a2c477 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -45,70 +45,74 @@
     // Grab all the groups since we don't have the cache available
     HashMap<AccountGroup.Id, AccountGroup.UUID> allGroups =
         new HashMap<AccountGroup.Id, AccountGroup.UUID>();
-    for( AccountGroup ag : db.accountGroups().all() ) {
+    for (AccountGroup ag : db.accountGroups().all()) {
       allGroups.put(ag.getId(), ag.getGroupUUID());
     }
 
     // Initialize some variables
     Connection conn = ((JdbcSchema) db).getConnection();
-    ArrayList<AccountGroupIncludeByUuid> newIncludes =
-        new ArrayList<AccountGroupIncludeByUuid>();
-    ArrayList<AccountGroupIncludeByUuidAudit> newIncludeAudits =
-        new ArrayList<AccountGroupIncludeByUuidAudit>();
+    ArrayList<AccountGroupById> newIncludes =
+        new ArrayList<AccountGroupById>();
+    ArrayList<AccountGroupByIdAud> newIncludeAudits =
+        new ArrayList<AccountGroupByIdAud>();
 
     // Iterate over all entries in account_group_includes
     Statement oldGroupIncludesStmt = conn.createStatement();
-    ResultSet oldGroupIncludes = oldGroupIncludesStmt.
-        executeQuery("SELECT * FROM account_group_includes");
-    while (oldGroupIncludes.next()) {
-      AccountGroup.Id oldGroupId =
-          new AccountGroup.Id(oldGroupIncludes.getInt("group_id"));
-      AccountGroup.Id oldIncludeId =
-          new AccountGroup.Id(oldGroupIncludes.getInt("include_id"));
-      AccountGroup.UUID uuidFromIncludeId = allGroups.get(oldIncludeId);
+    try {
+      ResultSet oldGroupIncludes = oldGroupIncludesStmt.
+          executeQuery("SELECT * FROM account_group_includes");
+      while (oldGroupIncludes.next()) {
+        AccountGroup.Id oldGroupId =
+            new AccountGroup.Id(oldGroupIncludes.getInt("group_id"));
+        AccountGroup.Id oldIncludeId =
+            new AccountGroup.Id(oldGroupIncludes.getInt("include_id"));
+        AccountGroup.UUID uuidFromIncludeId = allGroups.get(oldIncludeId);
 
-      // If we've got an include, but the group no longer exists, don't bother converting
-      if (uuidFromIncludeId == null) {
-        ui.message("Skipping group_id = \"" + oldIncludeId.get() +
-            "\", not a current group");
-        continue;
-      }
-
-      // Create the new include entry
-      AccountGroupIncludeByUuid destIncludeEntry = new AccountGroupIncludeByUuid(
-          new AccountGroupIncludeByUuid.Key(oldGroupId, uuidFromIncludeId));
-
-      // Iterate over all the audits (for this group)
-      PreparedStatement oldAuditsQuery = conn.prepareStatement(
-          "SELECT * FROM account_group_includes_audit WHERE group_id=? AND include_id=?");
-      oldAuditsQuery.setInt(1, oldGroupId.get());
-      oldAuditsQuery.setInt(2, oldIncludeId.get());
-      ResultSet oldGroupIncludeAudits = oldAuditsQuery.executeQuery();
-      while (oldGroupIncludeAudits.next()) {
-        Account.Id addedBy = new Account.Id(oldGroupIncludeAudits.getInt("added_by"));
-        int removedBy = oldGroupIncludeAudits.getInt("removed_by");
-
-        // Create the new audit entry
-        AccountGroupIncludeByUuidAudit destAuditEntry =
-            new AccountGroupIncludeByUuidAudit(destIncludeEntry, addedBy,
-                oldGroupIncludeAudits.getTimestamp("added_on"));
-
-        // If this was a "removed on" entry, note that
-        if (removedBy > 0) {
-          destAuditEntry.removed(new Account.Id(removedBy),
-              oldGroupIncludeAudits.getTimestamp("removed_on"));
+        // If we've got an include, but the group no longer exists, don't bother converting
+        if (uuidFromIncludeId == null) {
+          ui.message("Skipping group_id = \"" + oldIncludeId.get() +
+              "\", not a current group");
+          continue;
         }
-        newIncludeAudits.add(destAuditEntry);
+
+        // Create the new include entry
+        AccountGroupById destIncludeEntry = new AccountGroupById(
+            new AccountGroupById.Key(oldGroupId, uuidFromIncludeId));
+
+        // Iterate over all the audits (for this group)
+        PreparedStatement oldAuditsQueryStmt = conn.prepareStatement(
+            "SELECT * FROM account_group_includes_audit WHERE group_id=? AND include_id=?");
+        try {
+          oldAuditsQueryStmt.setInt(1, oldGroupId.get());
+          oldAuditsQueryStmt.setInt(2, oldIncludeId.get());
+          ResultSet oldGroupIncludeAudits = oldAuditsQueryStmt.executeQuery();
+          while (oldGroupIncludeAudits.next()) {
+            Account.Id addedBy = new Account.Id(oldGroupIncludeAudits.getInt("added_by"));
+            int removedBy = oldGroupIncludeAudits.getInt("removed_by");
+
+            // Create the new audit entry
+            AccountGroupByIdAud destAuditEntry =
+                new AccountGroupByIdAud(destIncludeEntry, addedBy,
+                    oldGroupIncludeAudits.getTimestamp("added_on"));
+
+            // If this was a "removed on" entry, note that
+            if (removedBy > 0) {
+              destAuditEntry.removed(new Account.Id(removedBy),
+                  oldGroupIncludeAudits.getTimestamp("removed_on"));
+            }
+            newIncludeAudits.add(destAuditEntry);
+          }
+          newIncludes.add(destIncludeEntry);
+        } finally {
+          oldAuditsQueryStmt.close();
+        }
       }
-      newIncludes.add(destIncludeEntry);
-      oldAuditsQuery.close();
-      oldGroupIncludeAudits.close();
+    } finally {
+      oldGroupIncludesStmt.close();
     }
-    oldGroupIncludes.close();
-    oldGroupIncludesStmt.close();
 
     // Now insert all of the new entries to the database
-    db.accountGroupIncludesByUuid().insert(newIncludes);
-    db.accountGroupIncludesByUuidAudit().insert(newIncludeAudits);
+    db.accountGroupById().insert(newIncludes);
+    db.accountGroupByIdAud().insert(newIncludeAudits);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
index 472bc25..80639d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
@@ -199,51 +199,40 @@
 
   static LegacyLabelTypes getLegacyTypes(ReviewDb db) throws SQLException {
     List<LegacyLabelType> types = Lists.newArrayListWithCapacity(2);
-    Statement catStmt = null;
-    PreparedStatement valStmt = null;
-    ResultSet catRs = null;
+    Statement catStmt = ((JdbcSchema) db).getConnection().createStatement();
     try {
-      catStmt = ((JdbcSchema) db).getConnection().createStatement();
-      catRs = catStmt.executeQuery(
+      ResultSet catRs = catStmt.executeQuery(
           "SELECT category_id, name, abbreviated_name, function_name, "
           + " copy_min_score"
           + " FROM approval_categories"
           + " ORDER BY position, name");
-      valStmt = ((JdbcSchema) db).getConnection().prepareStatement(
-          "SELECT value, name"
-          + " FROM approval_category_values"
-          + " WHERE category_id = ?");
-      while (catRs.next()) {
-        String id = catRs.getString("category_id");
-        valStmt.setString(1, id);
-        List<LabelValue> values = Lists.newArrayListWithCapacity(5);
-        ResultSet valRs = valStmt.executeQuery();
-        try {
+      PreparedStatement valStmt = ((JdbcSchema) db).getConnection().prepareStatement(
+              "SELECT value, name"
+                      + " FROM approval_category_values"
+                      + " WHERE category_id = ?");
+      try {
+        while (catRs.next()) {
+          String id = catRs.getString("category_id");
+          valStmt.setString(1, id);
+          List<LabelValue> values = Lists.newArrayListWithCapacity(5);
+          ResultSet valRs = valStmt.executeQuery();
           while (valRs.next()) {
             values.add(new LabelValue(
                 valRs.getShort("value"), valRs.getString("name")));
           }
-        } finally {
-          valRs.close();
+          LegacyLabelType type =
+              new LegacyLabelType(getLabelName(catRs.getString("name")), values);
+          type.setId(id);
+          type.setAbbreviation(catRs.getString("abbreviated_name"));
+          type.setFunctionName(catRs.getString("function_name"));
+          type.setCopyMinScore("Y".equals(catRs.getString("copy_min_score")));
+          types.add(type);
         }
-        LegacyLabelType type =
-            new LegacyLabelType(getLabelName(catRs.getString("name")), values);
-        type.setId(id);
-        type.setAbbreviation(catRs.getString("abbreviated_name"));
-        type.setFunctionName(catRs.getString("function_name"));
-        type.setCopyMinScore("Y".equals(catRs.getString("copy_min_score")));
-        types.add(type);
-      }
-    } finally {
-      if (valStmt != null) {
+      } finally {
         valStmt.close();
       }
-      if (catRs != null) {
-        catRs.close();
-      }
-      if (catStmt != null) {
-        catStmt.close();
-      }
+    } finally {
+      catStmt.close();
     }
     return new LegacyLabelTypes(types);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java
new file mode 100644
index 0000000..2cf8d2a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java
@@ -0,0 +1,26 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_80 extends SchemaVersion {
+
+  @Inject
+  Schema_80(Provider<Schema_79> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
new file mode 100644
index 0000000..bc3b390
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
@@ -0,0 +1,159 @@
+// 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.schema;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.sql.SQLException;
+
+public class Schema_81 extends SchemaVersion {
+
+  private final File pluginsDir;
+  private final GitRepositoryManager mgr;
+  private final AllProjectsName allProjects;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_81(Provider<Schema_80> prior, SitePaths sitePaths,
+      AllProjectsName allProjects, GitRepositoryManager mgr,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.pluginsDir = sitePaths.plugins_dir;
+    this.mgr = mgr;
+    this.allProjects = allProjects;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+      SQLException {
+    try {
+      migrateStartReplicationCapability(db, scanForReplicationPlugin());
+    } catch (RepositoryNotFoundException e) {
+      throw new OrmException(e);
+    } catch (SQLException e) {
+      throw new OrmException(e);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    } catch (ConfigInvalidException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  private File[] scanForReplicationPlugin() {
+    File[] matches = null;
+    if (pluginsDir != null && pluginsDir.exists()) {
+      matches = pluginsDir.listFiles(new FileFilter() {
+        @Override
+        public boolean accept(File pathname) {
+          String n = pathname.getName();
+          return (n.endsWith(".jar") || n.endsWith(".jar.disabled"))
+              && pathname.isFile() && n.indexOf("replication") >= 0;
+        }
+      });
+    }
+    return matches;
+  }
+
+  private void migrateStartReplicationCapability(ReviewDb db, File[] matches)
+      throws SQLException, RepositoryNotFoundException, IOException,
+      ConfigInvalidException {
+    Description d = new Description();
+    if (matches == null || matches.length == 0) {
+      d.what = Description.Action.REMOVE;
+    } else {
+      d.what = Description.Action.RENAME;
+      d.prefix = nameOf(matches[0]);
+    }
+    migrateStartReplicationCapability(db, d);
+  }
+
+  private void migrateStartReplicationCapability(ReviewDb db, Description d)
+      throws SQLException, RepositoryNotFoundException, IOException,
+      ConfigInvalidException {
+    Repository git = mgr.openRepository(allProjects);
+    try {
+      MetaDataUpdate md =
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
+      md.getCommitBuilder().setAuthor(serverUser);
+      md.getCommitBuilder().setCommitter(serverUser);
+      ProjectConfig config = ProjectConfig.read(md);
+      AccessSection capabilities =
+          config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES);
+      Permission startReplication =
+          capabilities.getPermission("startReplication");
+      if (startReplication == null) {
+        return;
+      }
+      String msg = null;
+      switch (d.what) {
+        case REMOVE:
+          capabilities.remove(startReplication);
+          msg = "Remove startReplication capability, plugin not installed\n";
+          break;
+        case RENAME:
+          capabilities.remove(startReplication);
+          Permission pluginStartReplication =
+              capabilities.getPermission(
+                  String.format("%s-startReplication", d.prefix), true);
+          pluginStartReplication.setRules(startReplication.getRules());
+          msg = "Rename startReplication capability to match updated plugin\n";
+          break;
+      }
+      config.replace(capabilities);
+      md.setMessage(msg);
+      config.commit(md);
+    } finally {
+      git.close();
+    }
+  }
+
+  private static String nameOf(File jar) {
+    String name = jar.getName();
+    if (name.endsWith(".disabled")) {
+      name = name.substring(0, name.lastIndexOf('.'));
+    }
+    int ext = name.lastIndexOf('.');
+    return 0 < ext ? name.substring(0, ext) : name;
+  }
+
+  private static class Description {
+    private enum Action {
+      REMOVE, RENAME
+    }
+    Action what;
+    String prefix;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java
new file mode 100644
index 0000000..939afe0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java
@@ -0,0 +1,152 @@
+// 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.schema;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectMySQL;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+import java.util.Set;
+
+public class Schema_82 extends SchemaVersion {
+
+  private Map<String,String> tables = ImmutableMap.of(
+      "account_group_includes_by_uuid", "account_group_by_id",
+      "account_group_includes_by_uuid_audit", "account_group_by_id_aud");
+
+  private Map<String,Index> indexes = ImmutableMap.of(
+      "account_project_watches_byProject",
+      new Index("account_project_watches", "account_project_watches_byP"),
+      "patch_set_approvals_closedByUser",
+      new Index("patch_set_approvals", "patch_set_approvals_closedByU"),
+      "submodule_subscription_access_bySubscription",
+      new Index("submodule_subscriptions", "submodule_subscr_acc_byS")
+      );
+
+  @Inject
+  Schema_82(Provider<Schema_81> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
+    final JdbcSchema s = (JdbcSchema) db;
+    final JdbcExecutor e = new JdbcExecutor(s);
+    renameTables(db, s, e);
+    renameColumn(db, s, e);
+    renameIndexes(db);
+  }
+
+  private void renameTables(final ReviewDb db, final JdbcSchema s,
+      final JdbcExecutor e) throws OrmException, SQLException {
+    SqlDialect dialect = ((JdbcSchema) db).getDialect();
+    final Set<String> existingTables = dialect.listTables(s.getConnection());
+    for (Map.Entry<String, String> entry : tables.entrySet()) {
+      // Does source table exist?
+      if (existingTables.contains(entry.getKey())) {
+        // Does target table exist?
+        if (!existingTables.contains(entry.getValue())) {
+          s.renameTable(e, entry.getKey(), entry.getValue());
+        }
+      }
+    }
+  }
+
+  private void renameColumn(final ReviewDb db, final JdbcSchema s,
+      final JdbcExecutor e) throws SQLException, OrmException {
+    SqlDialect dialect = ((JdbcSchema) db).getDialect();
+    final Set<String> existingColumns =
+        dialect.listColumns(s.getConnection(), "accounts");
+    // Does source column exist?
+    if (!existingColumns.contains("show_username_in_review_category")) {
+      return;
+    }
+    // Does target column exist?
+    if (existingColumns.contains("show_user_in_review")) {
+      return;
+    }
+    s.renameColumn(e, "accounts", "show_username_in_review_category",
+        "show_user_in_review");
+    // MySQL loose check constraint during the column renaming.
+    // Well it doesn't implemented anyway,
+    // check constraints are get parsed but do nothing
+    if (dialect instanceof DialectMySQL) {
+      Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+      try {
+        addCheckConstraint(stmt);
+      } finally {
+        stmt.close();
+      }
+    }
+  }
+
+  private void renameIndexes(ReviewDb db) throws SQLException {
+    SqlDialect dialect = ((JdbcSchema) db).getDialect();
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      // MySQL doesn't have alter index stmt, drop & create
+      if (dialect instanceof DialectMySQL) {
+        for (Map.Entry<String, Index> entry : indexes.entrySet()) {
+          stmt.executeUpdate("DROP INDEX " + entry.getKey() + " ON "
+              + entry.getValue().table);
+        }
+        stmt.executeUpdate("CREATE INDEX account_project_watches_byP ON " +
+            "account_project_watches (project_name)");
+        stmt.executeUpdate("CREATE INDEX patch_set_approvals_closedByU ON " +
+            "patch_set_approvals (change_open, account_id, change_sort_key)");
+        stmt.executeUpdate("CREATE INDEX submodule_subscr_acc_bys ON " +
+            "submodule_subscriptions (submodule_project_name, " +
+            "submodule_branch_name)");
+      } else {
+        for (Map.Entry<String, Index> entry : indexes.entrySet()) {
+          stmt.executeUpdate("ALTER INDEX " + entry.getKey() + " RENAME TO "
+              + entry.getValue().index);
+        }
+      }
+    } catch (SQLException e) {
+      // we don't care
+      // better we would check if index was already renamed
+      // gwtorm doesn't expose this functionality
+    } finally {
+      stmt.close();
+    }
+  }
+
+  private void addCheckConstraint(Statement stmt) throws SQLException {
+    // add check constraint for the destination column
+    stmt.executeUpdate("ALTER TABLE accounts ADD CONSTRAINT "
+        + "show_user_in_review_check CHECK "
+        + "(show_user_in_review IN('Y', 'N'))");
+  }
+
+  static class Index {
+    String table;
+    String index;
+
+    Index(String tableName, String indexName) {
+      this.table = tableName;
+      this.index = indexName;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java
new file mode 100644
index 0000000..160d41c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java
@@ -0,0 +1,26 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_83 extends SchemaVersion {
+
+  @Inject
+  Schema_83(Provider<Schema_82> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_84.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_84.java
new file mode 100644
index 0000000..c96f650
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_84.java
@@ -0,0 +1,26 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_84 extends SchemaVersion {
+
+  @Inject
+  Schema_84(Provider<Schema_83> 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 b5aead8..254a780 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,8 +14,10 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.base.CharMatcher;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.SqlDialect;
 import com.google.gwtorm.server.OrmException;
 
 import java.io.BufferedReader;
@@ -28,7 +30,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** Parses a SQL script from a resource file and later runs it. */
+/** Parses an SQL script from a resource file and later runs it. */
 class ScriptRunner {
   private final String name;
   private final List<String> commands;
@@ -49,11 +51,16 @@
 
   void run(final ReviewDb db) throws OrmException {
     try {
-      final Connection c = ((JdbcSchema) db).getConnection();
+      final JdbcSchema schema = (JdbcSchema)db;
+      final Connection c = schema.getConnection();
+      final SqlDialect dialect = schema.getDialect();
       final Statement stmt = c.createStatement();
       try {
         for (String sql : commands) {
           try {
+            if (!dialect.isStatementDelimiterSupported()) {
+              sql = CharMatcher.is(';').trimTrailingFrom(sql);
+            }
             stmt.execute(sql);
           } catch (SQLException e) {
             throw new OrmException("Error in " + name + ":\n" + sql, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAdvertisedAddresses.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAdvertisedAddresses.java
index 2219298..4a6eb29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAdvertisedAddresses.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAdvertisedAddresses.java
@@ -21,8 +21,8 @@
 import java.lang.annotation.Retention;
 
 /**
- * Marker on the list of {@link SocketAddress}es configured to be advertised by
- * the server.
+ * Marker on the list of {@link java.net.SocketAddress}es configured to be
+ * advertised by the server.
  */
 @Retention(RUNTIME)
 @BindingAnnotation
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshListenAddresses.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshListenAddresses.java
index be40567..a4e238d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshListenAddresses.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshListenAddresses.java
@@ -21,8 +21,8 @@
 import java.lang.annotation.Retention;
 
 /**
- * Marker on the list of {@link SocketAddress}es on which the SSH daemon is
- * configured to listen.
+ * Marker on the list of {@link java.net.SocketAddress}es on which the SSH
+ * daemon is configured to listen.
  */
 @Retention(RUNTIME)
 @BindingAnnotation
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index 4b5d736..0b2efd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.util;
 
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
@@ -31,8 +32,6 @@
 import java.util.Map;
 import java.util.concurrent.Callable;
 
-import javax.annotation.Nullable;
-
 /** Propagator for Guice's built-in servlet scope. */
 public class GuiceRequestScopePropagator extends RequestScopePropagator {
 
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
new file mode 100644
index 0000000..6730e30
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
@@ -0,0 +1,48 @@
+// 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.util;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.InternalUser;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Inject;
+
+/** RequestContext with an InternalUser making the internals visible. */
+public class ServerRequestContext implements RequestContext {
+  private final InternalUser user;
+
+  @Inject
+  ServerRequestContext(InternalUser.Factory userFactory) {
+    this.user = userFactory.create();
+  }
+
+  @Override
+  public CurrentUser getCurrentUser() {
+    return user;
+  }
+
+  @Override
+  public Provider<ReviewDb> getReviewDbProvider() {
+    return new Provider<ReviewDb>() {
+      @Override
+      public ReviewDb get() {
+        throw new ProvisionException(
+            "Automatic ReviewDb only available in request scope");
+      }
+    };
+  }
+}
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 bd43e9d..2a67c90 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.util;
 
 import com.google.common.base.Objects;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -27,8 +28,6 @@
 import com.google.inject.name.Named;
 import com.google.inject.name.Names;
 
-import javax.annotation.Nullable;
-
 /**
  * ThreadLocalRequestContext manages the current RequestContext using a
  * ThreadLocal. When the context is set, the fields exposed by the context
@@ -59,7 +58,7 @@
 
       @Provides
       IdentifiedUser provideCurrentUser(CurrentUser user) {
-        if (user instanceof IdentifiedUser) {
+        if (user.isIdentifiedUser()) {
           return (IdentifiedUser) user;
         }
         throw new ProvisionException(NotSignedInException.MESSAGE,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java
new file mode 100644
index 0000000..4c4b96e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java
@@ -0,0 +1,33 @@
+// 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.util;
+
+import org.joda.time.DateTimeUtils;
+
+import java.sql.Timestamp;
+
+/** Static utility methods for dealing with dates and times. */
+public class TimeUtil {
+  public static long nowMs() {
+    return DateTimeUtils.currentTimeMillis();
+  }
+
+  public static Timestamp nowTs() {
+    return new Timestamp(nowMs());
+  }
+
+  private TimeUtil() {
+  }
+}
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 11911bd..dd2a008 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
@@ -51,7 +51,7 @@
     CurrentUser curUser = cControl.getCurrentUser();
     Term resultTerm;
 
-    if (curUser instanceof IdentifiedUser) {
+    if (curUser.isIdentifiedUser()) {
       Account.Id id = ((IdentifiedUser)curUser).getAccountId();
       resultTerm = new IntegerTerm(id.get());
     } else if (curUser instanceof AnonymousUser) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 3f4b656..62fe075 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -112,7 +112,6 @@
   }
 
   private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
-    PrologEnvironment env = (PrologEnvironment) engine.control;
-    return env.getInjector().getInstance(IdentifiedUser.GenericFactory.class);
+    return ((PrologEnvironment) engine.control).getArgs().getUserFactory();
   }
 }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
index 161da79..698c11c 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -15,10 +15,8 @@
 package gerrit;
 
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -44,6 +42,8 @@
  * </ul>
  */
 class PRED_get_legacy_label_types_1 extends Predicate.P1 {
+  private static final SymbolTerm NONE = SymbolTerm.intern("none");
+
   PRED_get_legacy_label_types_1(Term a1, Operation n) {
     arg1 = a1;
     cont = n;
@@ -53,14 +53,8 @@
   public Operation exec(Prolog engine) throws PrologException {
     engine.setB0();
     Term a1 = arg1.dereference();
-
-    PrologEnvironment env = (PrologEnvironment) engine.control;
-    ProjectState state = env.getInjector().getInstance(ProjectCache.class)
-        .get(StoredValues.CHANGE.get(engine).getDest().getParentKey());
-    if (state == null) {
-      return engine.fail();
-    }
-    List<LabelType> list = state.getLabelTypes().getLabelTypes();
+    List<LabelType> list =
+        StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes().getLabelTypes();
     Term head = Prolog.Nil;
     for (int idx = list.size() - 1; 0 <= idx; idx--) {
       head = new ListTerm(export(list.get(idx)), head);
@@ -76,10 +70,12 @@
       "label_type", 4);
 
   static Term export(LabelType type) {
+    LabelValue min = type.getMin();
+    LabelValue max = type.getMax();
     return new StructureTerm(symLabelType,
         SymbolTerm.intern(type.getName()),
         SymbolTerm.intern(type.getFunctionName()),
-        new IntegerTerm(type.getMin().getValue()),
-        new IntegerTerm(type.getMax().getValue()));
+        min != null ? new IntegerTerm(min.getValue()) : NONE,
+        max != null ? new IntegerTerm(max.getValue()) : NONE);
   }
 }
diff --git a/gerrit-server/src/main/prolog/BUCK b/gerrit-server/src/main/prolog/BUCK
new file mode 100644
index 0000000..09a6553
--- /dev/null
+++ b/gerrit-server/src/main/prolog/BUCK
@@ -0,0 +1,8 @@
+include_defs('//lib/prolog/prolog.defs')
+
+prolog_cafe_library(
+  name = 'common',
+  srcs = ['gerrit_common.pl'],
+  deps = ['//gerrit-server:server'],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 71e7383..4738d15 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -239,6 +239,7 @@
 %% Apply the old -2..+2 style logic.
 %%
 legacy_submit_rule('MaxWithBlock', Label, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
+legacy_submit_rule('AnyWithBlock', Label, Min, Max, T) :- !, any_with_block(Label, Min, T).
 legacy_submit_rule('MaxNoBlock', Label, Min, Max, T) :- !, max_no_block(Label, Max, T).
 legacy_submit_rule('NoBlock', Label, Min, Max, T) :- !, T = may(_).
 legacy_submit_rule('NoOp', Label, Min, Max, T) :- !, T = may(_).
@@ -267,6 +268,7 @@
 max_with_block(Label, Min, Max, need(Max)) :-
   true
   .
+
 %TODO Uncomment this clause when group suggesting is possible.
 %max_with_block(Label, Min, Max, need(Max, Group)) :-
 %  \+ check_label_range_permission(Label, Max, ok(_)),
@@ -276,6 +278,16 @@
 %  \+ check_label_range_permission(Label, Max, ask(Group))
 %  .
 
+%% any_with_block:
+%%
+%% - The maximum is never used.
+%%
+any_with_block(Label, Min, reject(Who)) :-
+  check_label_range_permission(Label, Min, ok(Who)),
+  !
+  .
+any_with_block(Label, Min, may(_)).
+
 
 %% max_no_block:
 %%
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
new file mode 100644
index 0000000..a1b966f7
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -0,0 +1,16 @@
+accessDatabase = Access Database
+administrateServer = Administrate Server
+createAccount = Create Account
+createGroup = Create Group
+createProject = Create Project
+emailReviewers = Email Reviewers
+flushCaches = Flush Caches
+killTask = Kill Task
+priority = Priority
+queryLimit = Query Limit
+runAs = Run As
+runGC = Run Garbage Collection
+streamEvents = Stream Events
+viewCaches = View Caches
+viewConnections = View Connections
+viewQueue = View Queue
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
new file mode 100644
index 0000000..28f29fd
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
@@ -0,0 +1,33 @@
+## 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.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example".  If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used.  If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The Footer.vm template will determine the contents of the footer text
+## appended to the end of all outgoing emails after the ChangeFooter and
+## CommentFooter.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
index 42f2ca9..d524b48 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
@@ -46,7 +46,6 @@
 #if($email.changeUrl)
 
   $email.changeUrl
-
 #end
 #end
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
deleted file mode 100644
index e761627..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
+++ /dev/null
@@ -1,54 +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.
-##
-##
-## Template Type:
-## -------------
-## This is a velocity mail template, see: http://velocity.apache.org and the
-## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
-##
-## Template File Names and extensions:
-## ----------------------------------
-## Gerrit will use templates ending in ".vm" but will ignore templates ending
-## in ".vm.example".  If a .vm template does not exist, the default internal
-## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.example file to a .vm
-## file and edit it appropriately.
-##
-## This Template:
-## --------------
-## The RebasedPatchSet.vm template will determine the contents of the email
-## related to a user rebasing a patchset for a change through the Gerrit UI.
-## It is a ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
-##
-#if($email.reviewerNames)
-Hello $email.joinStrings($email.reviewerNames, ', '),
-
-I'd like you to reexamine a rebased change.#if($email.changeUrl)  Please visit
-
-    $email.changeUrl
-
-to look at the new rebased patch set (#$patchSet.patchSetId).
-#end
-#else
-$fromName has created a new patch set by issuing a rebase in Gerrit (#$patchSet.patchSetId).
-#end
-
-Change subject: $change.subject
-......................................................................
-
-$email.changeDetail
-#if($email.sshHost)
-  git pull ssh://$email.sshHost/$projectName $patchSet.refName
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index b29e25c..12dcd52 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -38,6 +38,11 @@
 		return
 	fi
 
+	if test "false" = "`git config --bool --get gerrit.createChangeId`"
+	then
+		return
+	fi
+
 	# Does Change-Id: already exist? if so, exit (no change).
 	if grep -i '^Change-Id:' "$MSG" >/dev/null
 	then
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 9b122eb..0d0ab1a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -14,115 +14,79 @@
 
 package com.google.gerrit.rules;
 
+import static com.google.gerrit.common.data.Permission.LABEL;
+import static com.google.gerrit.server.project.Util.value;
+import static com.google.gerrit.server.project.Util.category;
+import static com.google.gerrit.server.project.Util.REGISTERED;
+import static com.google.gerrit.server.project.Util.grant;
+
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.AbstractModule;
 
 import java.util.Arrays;
-import java.util.Set;
 
 public class GerritCommonTest extends PrologTestCase {
-  private Projects projects;
+  private final LabelType V = category("Verified",
+      value(1, "Verified"),
+      value(0, "No score"),
+      value(-1, "Fails"));
+  private final LabelType Q = category("Qualified",
+      value(1, "Qualified"),
+      value(0, "No score"),
+      value(-1, "Fails"));
+
+  private final Project.NameKey localKey = new Project.NameKey("local");
+  private ProjectConfig local;
+  private Util util;
 
   @Override
   public void setUp() throws Exception {
     super.setUp();
-    projects = new Projects(new LabelTypes(Arrays.asList(
-        category("Code-Review",
-            value(2, "Looks good to me, approved"),
-            value(1, "Looks good to me, but someone else must approve"),
-            value(0, "No score"),
-            value(-1, "I would prefer that you didn't submit this"),
-            value(-2, "Do not submit")),
-        category("Verified", value(1, "Verified"),
-            value(0, "No score"), value(-1, "Fails")))));
+    util = new Util();
     load("gerrit", "gerrit_common_test.pl", new AbstractModule() {
       @Override
       protected void configure() {
-        bind(ProjectCache.class).toInstance(projects);
+        bind(PrologEnvironment.Args.class).toInstance(
+            new PrologEnvironment.Args(
+                null,
+                null,
+                null,
+                null,
+                null,
+                null));
       }
     });
+
+    local = new ProjectConfig(localKey);
+    local.createInMemory();
+    Q.setRefPatterns(Arrays.asList("refs/heads/develop"));
+
+    local.getLabelSections().put(V.getName(), V);
+    local.getLabelSections().put(Q.getName(), Q);
+    util.add(local);
+    grant(local, LABEL + V.getName(), -1, +1, REGISTERED, "refs/heads/*");
+    grant(local, LABEL + Q.getName(), -1, +1, REGISTERED, "refs/heads/master");
   }
 
   @Override
   protected void setUpEnvironment(PrologEnvironment env) {
-    env.set(StoredValues.CHANGE, new Change(
-        new Change.Key("Ibeef"), new Change.Id(1), new Account.Id(2),
-        new Branch.NameKey(projects.allProjectsName, "master")));
+    Change change =
+        new Change(new Change.Key("Ibeef"), new Change.Id(1),
+            new Account.Id(2),
+            new Branch.NameKey(localKey, "refs/heads/master"),
+            TimeUtil.nowTs());
+    env.set(StoredValues.CHANGE, change);
+    env.set(StoredValues.CHANGE_CONTROL, util.user(local).controlFor(change));
   }
 
-  private static LabelValue value(int value, String text) {
-    return new LabelValue((short) value, text);
-  }
-
-  private static LabelType category(String name, LabelValue... values) {
-    return new LabelType(name, Arrays.asList(values));
-  }
-
-  private static class Projects implements ProjectCache {
-    private final AllProjectsName allProjectsName;
-    private final ProjectState allProjects;
-
-    private Projects(LabelTypes labelTypes) {
-      allProjectsName = new AllProjectsName("All-Projects");
-      ProjectConfig config = new ProjectConfig(allProjectsName);
-      config.createInMemory();
-      for (LabelType label : labelTypes.getLabelTypes()) {
-        config.getLabelSections().put(label.getName(), label);
-      }
-      allProjects = new ProjectState(null, this, allProjectsName, null, null,
-          null, null, null, config);
-    }
-
-    @Override
-    public ProjectState getAllProjects() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public ProjectState get(Project.NameKey projectName) {
-      assertEquals(allProjectsName, projectName);
-      return allProjects;
-    }
-
-    @Override
-    public void evict(Project p) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void remove(Project p) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Iterable<Project.NameKey> all() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Iterable<Project.NameKey> byName(String prefix) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void onCreateProject(Project.NameKey newProjectName) {
-      throw new UnsupportedOperationException();
-    }
+  public void testGerritCommon() {
+    runPrologBasedTests();
   }
 }
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 83809a4..df39003 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.rules;
 
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Guice;
 import com.google.inject.Module;
 
@@ -55,7 +56,7 @@
   protected void load(String pkg, String prologResource, Module... modules)
       throws CompileException, IOException {
     ArrayList<Module> moduleList = new ArrayList<Module>();
-    moduleList.add(new PrologModule());
+    moduleList.add(new PrologModule.EnvironmentModule());
     moduleList.addAll(Arrays.asList(modules));
 
     envFactory = Guice.createInjector(moduleList)
@@ -114,9 +115,9 @@
     return env.execute(Prolog.BUILTIN, "clause", head, new VariableTerm());
   }
 
-  public void testRunPrologTestCases() {
+  public void runPrologBasedTests() {
     int errors = 0;
-    long start = System.currentTimeMillis();
+    long start = TimeUtil.nowMs();
 
     for (Term test : tests) {
       PrologEnvironment env = envFactory.create(machine);
@@ -159,7 +160,7 @@
       }
     }
 
-    long end = System.currentTimeMillis();
+    long end = TimeUtil.nowMs();
     System.out.println("-------------------------------");
     System.out.format("Prolog tests: %d, Failures: %d, Time elapsed %.3f sec",
         tests.size(), errors, (end - start) / 1000.0);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
deleted file mode 100644
index 14e0ebf..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
+++ /dev/null
@@ -1,233 +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.change;
-
-import static org.easymock.EasyMock.anyBoolean;
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.replay;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.changes.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
-import com.google.gerrit.reviewdb.server.PatchSetAccess;
-import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.change.ChangeJson.ChangeMessageInfo;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Binder;
-import com.google.inject.Guice;
-import com.google.inject.Module;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-import org.easymock.IAnswer;
-import org.eclipse.jgit.lib.Config;
-
-import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-public class ChangeJsonTest extends TestCase {
-
-  public void testFormatChangeMessages() throws OrmException {
-
-    // create mocks
-    final CurrentUser currentUser = createMock(CurrentUser.class);
-    final GitRepositoryManager grm = createMock(GitRepositoryManager.class);
-    final AccountByEmailCache abec = createMock(AccountByEmailCache.class);
-    final AccountCache ac = createMock(AccountCache.class);
-    final AccountInfo.Loader.Factory alf =
-        createMock(AccountInfo.Loader.Factory.class);
-    final CapabilityControl.Factory ccf =
-        createMock(CapabilityControl.Factory.class);
-    final GroupBackend gb = createMock(GroupBackend.class);
-    final Realm r = createMock(Realm.class);
-    final PatchListCache plc = createMock(PatchListCache.class);
-    final ProjectCache pc = createMock(ProjectCache.class);
-    final Config config = new Config();  // unable to mock
-    final ReviewDb rdb = createMock(ReviewDb.class);
-    final ChangeAccess ca = createMock(ChangeAccess.class);
-    final PatchSetAccess psa = createMock(PatchSetAccess.class);
-    final PatchSetApprovalAccess psaa =
-        createMock(PatchSetApprovalAccess.class);
-    final ChangeMessageAccess cma = createMock(ChangeMessageAccess.class);
-    AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
-
-    // create ChangeJson instance
-    Module mod = new Module() {
-      @Override
-      public void configure(Binder binder) {
-        binder.bind(CurrentUser.class).toInstance(currentUser);
-        binder.bind(GitRepositoryManager.class).toInstance(grm);
-        binder.bind(AccountByEmailCache.class).toInstance(abec);
-        binder.bind(AccountCache.class).toInstance(ac);
-        binder.bind(AccountInfo.Loader.Factory.class).toInstance(alf);
-        binder.bind(CapabilityControl.Factory.class).toInstance(ccf);
-        binder.bind(GroupBackend.class).toInstance(gb);
-        binder.bind(Realm.class).toInstance(r);
-        binder.bind(PatchListCache.class).toInstance(plc);
-        binder.bind(ProjectCache.class).toInstance(pc);
-        binder.bind(ReviewDb.class).toInstance(rdb);
-        binder.bind(Config.class).annotatedWith(GerritServerConfig.class)
-            .toInstance(config);
-        binder.bind(String.class).annotatedWith(CanonicalWebUrl.class)
-            .toInstance("");
-        binder.bind(String.class).annotatedWith(AnonymousCowardName.class)
-            .toInstance("");
-      }
-    };
-    ChangeJson json = Guice.createInjector(mod).getInstance(ChangeJson.class);
-
-    // define mock behavior for tests
-    expect(alf.create(anyBoolean())).andReturn(accountLoader).anyTimes();
-
-    Project.NameKey proj = new Project.NameKey("ProjectNameKey");
-    Branch.NameKey forBranch = new Branch.NameKey(proj, "BranchNameKey");
-
-    Change.Key changeKey123 = new Change.Key("ChangeKey123");
-    Change.Id changeId123 = new Change.Id(123);
-    Change change123 = new Change(changeKey123, changeId123, null, forBranch);
-
-    Change.Key changeKey234 = new Change.Key("ChangeKey234");
-    Change.Id changeId234 = new Change.Id(234);
-    Change change234 = new Change(changeKey234, changeId234, null, forBranch);
-
-    expect(ca.get(Sets.newHashSet(changeId123)))
-        .andAnswer(results(Change.class, change123)).anyTimes();
-    expect(ca.get(changeId123)).andReturn(change123).anyTimes();
-    expect(ca.get(Sets.newHashSet(changeId234)))
-        .andAnswer(results(Change.class, change234));
-    expect(ca.get(changeId234)).andReturn(change234);
-    expect(rdb.changes()).andReturn(ca).anyTimes();
-
-    expect(psa.get(EasyMock.<Iterable<PatchSet.Id>>anyObject()))
-        .andAnswer(results(PatchSet.class)).anyTimes();
-    expect(rdb.patchSets()).andReturn(psa).anyTimes();
-
-    expect(psaa.byPatchSet(anyObject(PatchSet.Id.class)))
-        .andAnswer(results(PatchSetApproval.class)).anyTimes();
-    expect(rdb.patchSetApprovals()).andReturn(psaa).anyTimes();
-
-    expect(currentUser.getStarredChanges())
-        .andReturn(Collections.<Change.Id>emptySet()).anyTimes();
-
-    long timeBase = System.currentTimeMillis();
-    ChangeMessage changeMessage1 =changeMessage(
-        changeId123, "cm1", 111, timeBase, 1111, "first message");
-    ChangeMessage changeMessage2 = changeMessage(
-        changeId123, "cm2", 222, timeBase + 1000, 1111, "second message");
-    expect(cma.byChange(changeId123))
-        .andAnswer(results(ChangeMessage.class, changeMessage2, changeMessage1))
-        .anyTimes();
-    expect(cma.byChange(changeId234)).andAnswer(results(ChangeMessage.class));
-    expect(rdb.changeMessages()).andReturn(cma).anyTimes();
-
-    expect(accountLoader.get(anyObject(Account.Id.class)))
-        .andAnswer(accountForId()).anyTimes();
-    accountLoader.fill();
-    expectLastCall().anyTimes();
-
-    replay(rdb, ca, psa, psaa, alf, currentUser, cma, accountLoader);
-
-    // test 1: messages not returned by default
-    ChangeInfo ci = json.format(new ChangeData(changeId123));
-    assertNull(ci.messages);
-
-    json.addOption(ListChangesOption.MESSAGES);
-
-    // test 2: two change messages, in chronological order
-    ci = json.format(new ChangeData(changeId123));
-    assertNotNull(ci.messages);
-    assertEquals(2, ci.messages.size());
-    Iterator<ChangeMessageInfo> cmis = ci.messages.iterator();
-    assertEquals(changeMessage1, cmis.next());
-    assertEquals(changeMessage2, cmis.next());
-
-    // test 3: no change messages
-    ci = json.format(new ChangeData(changeId234));
-    assertNotNull(ci.messages);
-    assertEquals(0, ci.messages.size());
-  }
-
-  private static IAnswer<AccountInfo> accountForId() {
-    return new IAnswer<AccountInfo>() {
-      @Override
-      public AccountInfo answer() throws Throwable {
-        Account.Id id = (Account.Id) EasyMock.getCurrentArguments()[0];
-        AccountInfo ai = new AccountInfo(id);
-        return ai;
-      }};
-  }
-
-  private static <T> IAnswer<ResultSet<T>> results(Class<T> type, T... items) {
-    final List<T> list = Lists.newArrayList(items);
-    return new IAnswer<ResultSet<T>>() {
-      @Override
-      public ResultSet<T> answer() throws Throwable {
-        return new ListResultSet<T>(list);
-      }};
-  }
-
-  private static void assertEquals(ChangeMessage cm, ChangeMessageInfo cmi) {
-    assertEquals(cm.getPatchSetId().get(), (int) cmi._revisionNumber);
-    assertEquals(cm.getMessage(), cmi.message);
-    assertEquals(cm.getKey().get(), cmi.id);
-    assertEquals(cm.getWrittenOn(), cmi.date);
-    assertNotNull(cmi.author);
-    assertEquals(cm.getAuthor(), cmi.author._id);
-  }
-
-  private static ChangeMessage changeMessage(Change.Id changeId,
-      String uuid, int accountId, long time, int psId, String message) {
-    ChangeMessage.Key key = new ChangeMessage.Key(changeId, uuid);
-    Account.Id author = new Account.Id(accountId);
-    Timestamp updated = new Timestamp(time);
-    PatchSet.Id ps = new PatchSet.Id(changeId, psId);
-    ChangeMessage changeMessage = new ChangeMessage(key, author, updated, ps);
-    changeMessage.setMessage(message);
-    return changeMessage;
-  }
-}
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 21d1ce4..18b3c98 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
@@ -22,6 +22,7 @@
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -29,6 +30,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+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.PatchLineComment.Status;
@@ -36,7 +38,7 @@
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.change.CommentInfo.Side;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.AbstractModule;
@@ -108,16 +110,16 @@
     PatchSet ps2 = new PatchSet(psId2);
     expect(revRes2.getPatchSet()).andReturn(ps2);
 
-    long timeBase = System.currentTimeMillis();
+    long timeBase = TimeUtil.nowMs();
     plc1 = newPatchLineComment(psId1, "Comment1", null,
         "FileOne.txt", Side.REVISION, 1, account1, timeBase,
-        "First Comment");
+        "First Comment", new CommentRange(1, 2, 3, 4));
     plc2 = newPatchLineComment(psId1, "Comment2", "Comment1",
         "FileOne.txt", Side.REVISION, 1, account2, timeBase + 1000,
-        "Reply to First Comment");
+        "Reply to First Comment",  new CommentRange(1, 2, 3, 4));
     plc3 = newPatchLineComment(psId1, "Comment3", "Comment1",
         "FileOne.txt", Side.PARENT, 1, account1, timeBase + 2000,
-        "First Parent Comment");
+        "First Parent Comment",  new CommentRange(1, 2, 3, 4));
 
     expect(plca.publishedByPatchSet(psId1))
         .andAnswer(results(plc1, plc2, plc3)).anyTimes();
@@ -206,17 +208,19 @@
     assertEquals(plc.getSide() == 0 ? Side.PARENT : Side.REVISION,
         Objects.firstNonNull(ci.side, Side.REVISION));
     assertEquals(plc.getWrittenOn(), ci.updated);
+    assertEquals(plc.getRange(), ci.range);
   }
 
   private static PatchLineComment newPatchLineComment(PatchSet.Id psId,
       String uuid, String inReplyToUuid, String filename, Side side, int line,
-      Account.Id authorId, long millis, String message) {
+      Account.Id authorId, long millis, String message, CommentRange range) {
     Patch.Key p = new Patch.Key(psId, filename);
     PatchLineComment.Key id = new PatchLineComment.Key(p, uuid);
     PatchLineComment plc =
-        new PatchLineComment(id, line, authorId, inReplyToUuid);
+        new PatchLineComment(id, line, authorId, inReplyToUuid, TimeUtil.nowTs());
     plc.setMessage(message);
-    plc.setSide(side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+    plc.setRange(range);
+    plc.setSide(side == Side.PARENT ? (short) 0 : (short) 1);
     plc.setStatus(Status.PUBLISHED);
     plc.setWrittenOn(new Timestamp(millis));
     return plc;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
new file mode 100644
index 0000000..ae58819
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -0,0 +1,205 @@
+// 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.change;
+
+import com.google.gerrit.common.data.IncludedInDetail;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class IncludedInResolverTest extends RepositoryTestCase {
+
+  // Branch names
+  private static final String BRANCH_MASTER = "master";
+  private static final String BRANCH_1_0 = "rel-1.0";
+  private static final String BRANCH_1_3 = "rel-1.3";
+  private static final String BRANCH_2_0 = "rel-2.0";
+  private static final String BRANCH_2_5 = "rel-2.5";
+
+  // Tag names
+  private static final String TAG_1_0 = "1.0";
+  private static final String TAG_1_0_1 = "1.0.1";
+  private static final String TAG_1_3 = "1.3";
+  private static final String TAG_2_0_1 = "2.0.1";
+  private static final String TAG_2_0 = "2.0";
+  private static final String TAG_2_5 = "2.5";
+  private static final String TAG_2_5_ANNOTATED = "2.5-annotated";
+  private static final String TAG_2_5_ANNOTATED_TWICE = "2.5-annotated_twice";
+
+  // Commits
+  private RevCommit commit_initial;
+  private RevCommit commit_v1_3;
+  private RevCommit commit_v2_5;
+
+  private List<String> expTags = new ArrayList<String>();
+  private List<String> expBranches = new ArrayList<String>();
+
+  private RevWalk revWalk;
+
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    /*- The following graph will be created.
+
+      o   tag 2.5, 2.5_annotated, 2.5_annotated_twice
+      |\
+      | o tag 2.0.1
+      | o tag 2.0
+      o | tag 1.3
+      |/
+      o   c3
+
+      | o tag 1.0.1
+      |/
+      o   tag 1.0
+      o   c2
+      o   c1
+
+     */
+
+    Git git = new Git(db);
+    revWalk = new RevWalk(db);
+    // Version 1.0
+    commit_initial = git.commit().setMessage("c1").call();
+    git.commit().setMessage("c2").call();
+    RevCommit commit_v1_0 = git.commit().setMessage("version 1.0").call();
+    git.tag().setName(TAG_1_0).setObjectId(commit_v1_0).call();
+    RevCommit c3 = git.commit().setMessage("c3").call();
+    // Version 1.01
+    createAndCheckoutBranch(commit_v1_0, BRANCH_1_0);
+    RevCommit commit_v1_0_1 =
+        git.commit().setMessage("verREFS_HEADS_RELsion 1.0.1").call();
+    git.tag().setName(TAG_1_0_1).setObjectId(commit_v1_0_1).call();
+    // Version 1.3
+    createAndCheckoutBranch(c3, BRANCH_1_3);
+    commit_v1_3 = git.commit().setMessage("version 1.3").call();
+    git.tag().setName(TAG_1_3).setObjectId(commit_v1_3).call();
+    // Version 2.0
+    createAndCheckoutBranch(c3, BRANCH_2_0);
+    RevCommit commit_v2_0 = git.commit().setMessage("version 2.0").call();
+    git.tag().setName(TAG_2_0).setObjectId(commit_v2_0).call();
+    RevCommit commit_v2_0_1 = git.commit().setMessage("version 2.0.1").call();
+    git.tag().setName(TAG_2_0_1).setObjectId(commit_v2_0_1).call();
+
+    // Version 2.5
+    createAndCheckoutBranch(commit_v1_3, BRANCH_2_5);
+    git.merge().include(commit_v2_0_1).setCommit(false)
+        .setFastForward(FastForwardMode.NO_FF).call();
+    commit_v2_5 = git.commit().setMessage("version 2.5").call();
+    git.tag().setName(TAG_2_5).setObjectId(commit_v2_5).setAnnotated(false)
+        .call();
+    Ref ref_tag_2_5_annotated =
+        git.tag().setName(TAG_2_5_ANNOTATED).setObjectId(commit_v2_5)
+            .setAnnotated(true).call();
+    RevTag tag_2_5_annotated =
+        revWalk.parseTag(ref_tag_2_5_annotated.getObjectId());
+    git.tag().setName(TAG_2_5_ANNOTATED_TWICE).setObjectId(tag_2_5_annotated)
+        .setAnnotated(true).call();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    revWalk.release();
+    super.tearDown();
+  }
+
+  @Test
+  public void resolveLatestCommit() throws Exception {
+    // Check tip commit
+    IncludedInDetail detail = resolve(commit_v2_5);
+
+    // Check that only tags and branches which refer the tip are returned
+    expTags.add(TAG_2_5);
+    expTags.add(TAG_2_5_ANNOTATED);
+    expTags.add(TAG_2_5_ANNOTATED_TWICE);
+    assertEquals(expTags, detail.getTags());
+    expBranches.add(BRANCH_2_5);
+    assertEquals(expBranches, detail.getBranches());
+  }
+
+  @Test
+  public void resolveFirstCommit() throws Exception {
+    // Check first commit
+    IncludedInDetail detail = resolve(commit_initial);
+
+    // Check whether all tags and branches are returned
+    expTags.add(TAG_1_0);
+    expTags.add(TAG_1_0_1);
+    expTags.add(TAG_1_3);
+    expTags.add(TAG_2_0);
+    expTags.add(TAG_2_0_1);
+    expTags.add(TAG_2_5);
+    expTags.add(TAG_2_5_ANNOTATED);
+    expTags.add(TAG_2_5_ANNOTATED_TWICE);
+    assertEquals(expTags, detail.getTags());
+
+    expBranches.add(BRANCH_MASTER);
+    expBranches.add(BRANCH_1_0);
+    expBranches.add(BRANCH_1_3);
+    expBranches.add(BRANCH_2_0);
+    expBranches.add(BRANCH_2_5);
+    assertEquals(expBranches, detail.getBranches());
+  }
+
+  @Test
+  public void resolveBetwixtCommit() throws Exception {
+    // Check a commit somewhere in the middle
+    IncludedInDetail detail = resolve(commit_v1_3);
+
+    // Check whether all succeeding tags and branches are returned
+    expTags.add(TAG_1_3);
+    expTags.add(TAG_2_5);
+    expTags.add(TAG_2_5_ANNOTATED);
+    expTags.add(TAG_2_5_ANNOTATED_TWICE);
+    assertEquals(expTags, detail.getTags());
+
+    expBranches.add(BRANCH_1_3);
+    expBranches.add(BRANCH_2_5);
+    assertEquals(expBranches, detail.getBranches());
+  }
+
+  private IncludedInDetail resolve(RevCommit commit) throws Exception {
+    return IncludedInResolver.resolve(db, revWalk, commit);
+  }
+
+  private void assertEquals(List<String> list1, List<String> list2) {
+    Collections.sort(list1);
+    Collections.sort(list2);
+    Assert.assertEquals(list1, list2);
+  }
+
+  private void createAndCheckoutBranch(ObjectId objectId, String branchName)
+      throws IOException {
+    String fullBranchName = "refs/heads/" + branchName;
+    super.createBranch(objectId, fullBranchName);
+    super.checkoutBranch(fullBranchName);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java
new file mode 100644
index 0000000..992502f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -0,0 +1,73 @@
+// 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.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.config.ListCapabilities.CapabilityInfo;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class ListCapabilitiesTest {
+  private Injector injector;
+
+  @Before
+  public void setUp() throws Exception {
+    AbstractModule mod = new AbstractModule() {
+      @Override
+      protected void configure() {
+        DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+        bind(CapabilityDefinition.class)
+          .annotatedWith(Exports.named("printHello"))
+          .toInstance(new CapabilityDefinition() {
+            @Override
+            public String getDescription() {
+              return "Print Hello";
+            }
+          });
+      }
+    };
+    injector = Guice.createInjector(mod);
+  }
+
+  @Test
+  public void testList() throws Exception {
+    Map<String, CapabilityInfo> m =
+        injector.getInstance(ListCapabilities.class)
+            .apply(new ConfigResource());
+    for (String id : GlobalCapability.getAllNames()) {
+      assertTrue("contains " + id, m.containsKey(id));
+      assertEquals(id, m.get(id).id);
+      assertNotNull(id + " has name", m.get(id).name);
+    }
+
+    String pluginCapability = "gerrit-printHello";
+    assertTrue("contains " + pluginCapability, m.containsKey(pluginCapability));
+    assertEquals(pluginCapability, m.get(pluginCapability).id);
+    assertEquals("Print Hello", m.get(pluginCapability).name);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
index 9a8fc00..0087df6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
@@ -21,10 +21,9 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.util.UUID;
 
 public class SitePathsTest extends TestCase {
-  public void testCreate_NotExisting() throws FileNotFoundException {
+  public void testCreate_NotExisting() throws IOException {
     final File root = random();
     final SitePaths site = new SitePaths(root);
     assertTrue(site.isNew);
@@ -32,7 +31,7 @@
     assertEquals(new File(root, "etc"), site.etc_dir);
   }
 
-  public void testCreate_Empty() throws FileNotFoundException {
+  public void testCreate_Empty() throws IOException {
     final File root = random();
     try {
       assertTrue(root.mkdir());
@@ -91,8 +90,11 @@
     assertEquals(new File(pfx + "a").getCanonicalFile(), site.resolve(pfx + "a"));
   }
 
-  private File random() {
-    final File t = new File("target");
-    return new File(t, "random-name-" + UUID.randomUUID().toString());
+  private static File random() throws IOException {
+    File tmp = File.createTempFile("gerrit_test_", "_site");
+    if (!tmp.delete()) {
+      throw new IOException("Cannot create " + tmp.getPath());
+    }
+    return tmp;
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index a849e68..0d5207a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -159,9 +159,8 @@
         + "[contributor-agreement \"Individual\"]\n" //
         + "  description = A new description\n" //
         + "  accepted = group Staff\n" //
-        + "  agreementUrl = http://www.example.com/agree\n" //
-        + "[project]\n"//
-        + "\tstate = active\n", text(rev, "project.config"));
+        + "  agreementUrl = http://www.example.com/agree\n",
+        text(rev, "project.config"));
   }
 
   @Test
@@ -188,9 +187,7 @@
         + "  submit = group People Who Can Submit\n" //
         + "\tsubmit = group Staff\n" //
         + "  upload = group Developers\n" //
-        + "  read = group Developers\n"//
-        + "[project]\n"//
-        + "\tstate = active\n", text(rev, "project.config"));
+        + "  read = group Developers\n", text(rev, "project.config"));
   }
 
   private ProjectConfig read(RevCommit rev) throws IOException,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index 322ffe2..e9a4cf3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.ResultSet;
@@ -608,9 +609,9 @@
 
     final CodeReviewCommit codeReviewCommit =
         new CodeReviewCommit(sourceMergeTip.toObjectId());
-    final Change submittedChange =
-        new Change(new Change.Key(sourceMergeTip.toObjectId().getName()),
-            new Change.Id(1), new Account.Id(1), sourceBranchNameKey);
+    final Change submittedChange = new Change(
+        new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
+        new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
     codeReviewCommit.change = submittedChange;
 
     final Map<Change.Id, CodeReviewCommit> mergedCommits =
@@ -712,9 +713,9 @@
 
     final CodeReviewCommit codeReviewCommit =
         new CodeReviewCommit(sourceMergeTip.toObjectId());
-    final Change submittedChange =
-        new Change(new Change.Key(sourceMergeTip.toObjectId().getName()),
-            new Change.Id(1), new Account.Id(1), sourceBranchNameKey);
+    final Change submittedChange = new Change(
+        new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
+        new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
     codeReviewCommit.change = submittedChange;
 
     final Map<Change.Id, CodeReviewCommit> mergedCommits =
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
new file mode 100644
index 0000000..d8a0f65
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+class FakeIndex implements ChangeIndex {
+  static Schema<ChangeData> V1 = new Schema<ChangeData>(1, false,
+    ImmutableList.<FieldDef<ChangeData, ?>> of(
+      ChangeField.STATUS));
+
+  static Schema<ChangeData> V2 = new Schema<ChangeData>(2, false,
+    ImmutableList.of(
+      ChangeField.STATUS,
+      ChangeField.FILE,
+      ChangeField.SORTKEY));
+
+  private static class Source implements ChangeDataSource {
+    private final Predicate<ChangeData> p;
+
+    Source(Predicate<ChangeData> p) {
+      this.p = p;
+    }
+
+    @Override
+    public int getCardinality() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasChange() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toString() {
+      return p.toString();
+    }
+  }
+
+  private final Schema<ChangeData> schema;
+
+  FakeIndex(Schema<ChangeData> schema) {
+    this.schema = schema;
+  }
+
+  @Override
+  public void insert(ChangeData cd) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void replace(ChangeData cd) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void delete(ChangeData cd) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void deleteAll() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+      throws QueryParseException {
+    return new FakeIndex.Source(p);
+  }
+
+  @Override
+  public Schema<ChangeData> getSchema() {
+    return schema;
+  }
+
+  @Override
+  public void close() {
+  }
+
+  @Override
+  public void markReady(boolean ready) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
new file mode 100644
index 0000000..63e62a0
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gwtorm.server.OrmException;
+
+public class FakeQueryBuilder extends ChangeQueryBuilder {
+  FakeQueryBuilder(IndexCollection indexes) {
+    super(
+        new FakeQueryBuilder.Definition<ChangeData, FakeQueryBuilder>(
+          FakeQueryBuilder.class),
+        new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
+          null, null, null, null, null, indexes),
+        null);
+  }
+
+  @Operator
+  public Predicate<ChangeData> foo(String value) {
+    return predicate("foo", value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> bar(String value) {
+    return predicate("bar", value);
+  }
+
+  private Predicate<ChangeData> predicate(String name, String value) {
+    return new OperatorPredicate<ChangeData>(name, value) {
+      @Override
+      public boolean match(ChangeData object) throws OrmException {
+        return false;
+      }
+
+      @Override
+      public int getCost() {
+        return 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
new file mode 100644
index 0000000..d9016e9
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
@@ -0,0 +1,223 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
+import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
+import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
+import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
+import static com.google.gerrit.reviewdb.client.Change.Status.SUBMITTED;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.RewritePredicate;
+import com.google.gerrit.server.query.change.AndSource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
+
+import junit.framework.TestCase;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+@SuppressWarnings("unchecked")
+public class IndexRewriteTest extends TestCase {
+  private FakeIndex index;
+  private IndexCollection indexes;
+  private ChangeQueryBuilder queryBuilder;
+  private IndexRewriteImpl rewrite;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    index = new FakeIndex(FakeIndex.V2);
+    indexes = new IndexCollection();
+    indexes.setSearchIndex(index);
+    queryBuilder = new FakeQueryBuilder(indexes);
+    rewrite = new IndexRewriteImpl(
+        indexes,
+        null,
+        new IndexRewriteImpl.BasicRewritesImpl(null, indexes),
+        new SqlRewriterImpl(null));
+  }
+
+  public void testIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("file:a");
+    assertEquals(query(in), rewrite(in));
+  }
+
+  public void testNonIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a");
+    assertSame(in, rewrite(in));
+  }
+
+  public void testIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("file:a file:b");
+    assertEquals(query(in), rewrite(in));
+  }
+
+  public void testNonIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a OR foo:b");
+    assertEquals(in, rewrite(in));
+  }
+
+  public void testOneIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndSource.class, out.getClass());
+    assertEquals(
+        ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
+        out.getChildren());
+  }
+
+  public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("-status:abandoned (status:open OR status:merged)");
+    assertEquals(
+        query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
+        rewrite.rewrite(in));
+  }
+
+  public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
+    Predicate<ChangeData> out = rewrite(in);
+    assertEquals(AndSource.class, out.getClass());
+    assertEquals(
+        ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
+        out.getChildren());
+  }
+
+  public void testMultipleIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("file:a OR foo:b OR file:c OR foo:d");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(OrSource.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          query(Predicate.or(in.getChild(0), in.getChild(2))),
+          in.getChild(1), in.getChild(3)),
+        out.getChildren());
+  }
+
+  public void testIndexAndNonIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("status:new bar:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndSource.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          query(Predicate.and(in.getChild(0), in.getChild(2))),
+          in.getChild(1)),
+        out.getChildren());
+  }
+
+  public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR status:draft) bar:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndSource.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          query(Predicate.and(in.getChild(0), in.getChild(2))),
+          in.getChild(1)),
+        out.getChildren());
+  }
+
+  public void testDuplicateCompoundIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR file:a) bar:p file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndSource.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          query(Predicate.and(in.getChild(0), in.getChild(2))),
+          in.getChild(1)),
+        out.getChildren());
+  }
+
+  public void testLimit() throws Exception {
+    Predicate<ChangeData> in = parse("file:a limit:3");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndSource.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          query(in.getChild(0), 3),
+          in.getChild(1)),
+        out.getChildren());
+  }
+
+  public void testGetPossibleStatus() throws Exception {
+    assertEquals(EnumSet.allOf(Change.Status.class), status("file:a"));
+    assertEquals(EnumSet.of(NEW), status("is:new"));
+    assertEquals(EnumSet.of(SUBMITTED, DRAFT, MERGED, ABANDONED),
+        status("-is:new"));
+    assertEquals(EnumSet.of(NEW, MERGED), status("is:new OR is:merged"));
+
+    EnumSet<Change.Status> none = EnumSet.noneOf(Change.Status.class);
+    assertEquals(none, status("is:new is:merged"));
+    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
+    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
+
+    assertEquals(EnumSet.of(MERGED, SUBMITTED),
+        status("(is:new is:draft) OR (is:merged OR is:submitted)"));
+  }
+
+  public void testUnsupportedIndexOperator() throws Exception {
+    Predicate<ChangeData> in = parse("status:merged file:a");
+    assertEquals(query(in), rewrite(in));
+
+    indexes.setSearchIndex(new FakeIndex(FakeIndex.V1));
+    Predicate<ChangeData> out = rewrite(in);
+    assertTrue(out instanceof AndPredicate);
+    assertEquals(ImmutableList.of(
+          query(in.getChild(0)),
+          in.getChild(1)),
+        out.getChildren());
+  }
+
+  public void testNoChangeIndexUsesSqlRewrites() throws Exception {
+    Predicate<ChangeData> in = parse("status:open project:p ref:b");
+    Predicate<ChangeData> out;
+
+    out = rewrite(in);
+    assertTrue(out instanceof AndPredicate || out instanceof IndexedChangeQuery);
+
+    indexes.setSearchIndex(null);
+    out = rewrite(in);
+    assertTrue(out instanceof RewritePredicate);
+  }
+
+  private Predicate<ChangeData> parse(String query) throws QueryParseException {
+    return queryBuilder.parse(query);
+  }
+
+  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+      throws QueryParseException {
+    return rewrite.rewrite(in);
+  }
+
+  private IndexedChangeQuery query(Predicate<ChangeData> p)
+      throws QueryParseException {
+    return query(p, IndexRewriteImpl.MAX_LIMIT);
+  }
+
+  private IndexedChangeQuery query(Predicate<ChangeData> p, int limit)
+      throws QueryParseException {
+    return new IndexedChangeQuery(null, index, p, limit);
+  }
+
+  private Set<Change.Status> status(String query) throws QueryParseException {
+    return IndexRewriteImpl.getPossibleStatus(parse(query));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexedChangeQueryTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexedChangeQueryTest.java
new file mode 100644
index 0000000..81518f5
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexedChangeQueryTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.gerrit.server.index.IndexedChangeQuery.replaceSortKeyPredicates;
+
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+
+import junit.framework.TestCase;
+
+public class IndexedChangeQueryTest extends TestCase {
+  private FakeIndex index;
+  private ChangeQueryBuilder queryBuilder;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    index = new FakeIndex(FakeIndex.V2);
+    IndexCollection indexes = new IndexCollection();
+    indexes.setSearchIndex(index);
+    queryBuilder = new FakeQueryBuilder(indexes);
+  }
+
+  public void testReplaceSortKeyPredicate_NoSortKey() throws Exception {
+    Predicate<ChangeData> p = parse("foo:a bar:b OR (foo:b bar:a)");
+    assertSame(p, replaceSortKeyPredicates(p, "1234"));
+  }
+
+  public void testReplaceSortKeyPredicate_TopLevelSortKey() throws Exception {
+    Predicate<ChangeData> p;
+    p = parse("foo:a bar:b sortkey_before:1234 OR (foo:b bar:a)");
+    assertEquals(parse("foo:a bar:b sortkey_before:5678 OR (foo:b bar:a)"),
+        replaceSortKeyPredicates(p, "5678"));
+    p = parse("foo:a bar:b sortkey_after:1234 OR (foo:b bar:a)");
+    assertEquals(parse("foo:a bar:b sortkey_after:5678 OR (foo:b bar:a)"),
+        replaceSortKeyPredicates(p, "5678"));
+  }
+
+  public void testReplaceSortKeyPredicate_NestedSortKey() throws Exception {
+    Predicate<ChangeData> p;
+    p = parse("foo:a bar:b OR (foo:b bar:a AND sortkey_before:1234)");
+    assertEquals(parse("foo:a bar:b OR (foo:b bar:a sortkey_before:5678)"),
+        replaceSortKeyPredicates(p, "5678"));
+    p = parse("foo:a bar:b OR (foo:b bar:a AND sortkey_after:1234)");
+    assertEquals(parse("foo:a bar:b OR (foo:b bar:a sortkey_after:5678)"),
+        replaceSortKeyPredicates(p, "5678"));
+  }
+
+  private Predicate<ChangeData> parse(String query) throws QueryParseException {
+    return queryBuilder.parse(query);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
index 2d432e6..3b4005c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.ioutil;
 
-import com.google.gerrit.server.ioutil.ColumnFormatter;
-
 import junit.framework.TestCase;
 
 import java.io.PrintWriter;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index 2db2b67..721059c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.util.TimeUtil;
 
 import junit.framework.TestCase;
 
@@ -276,7 +277,7 @@
 
   private AccountState makeUser(final String name, final String email) {
     final Account.Id userId = new Account.Id(42);
-    final Account account = new Account(userId);
+    final Account account = new Account(userId, TimeUtil.nowTs());
     account.setFullName(name);
     account.setPreferredEmail(email);
     final AccountState s =
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 a64039c..8f4e058 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
@@ -20,59 +20,63 @@
 import static com.google.gerrit.common.data.Permission.PUSH;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.common.data.Permission.SUBMIT;
+import static com.google.gerrit.server.project.Util.ANONYMOUS;
+import static com.google.gerrit.server.project.Util.REGISTERED;
+import static com.google.gerrit.server.project.Util.ADMIN;
+import static com.google.gerrit.server.project.Util.DEVS;
+import static com.google.gerrit.server.project.Util.grant;
+import static com.google.gerrit.server.project.Util.doNotInherit;
 
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.rules.PrologEnvironment;
-import com.google.gerrit.rules.RulesCache;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
 
 import junit.framework.TestCase;
 
-import org.eclipse.jgit.lib.Config;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
 public class RefControlTest extends TestCase {
-  public void testOwnerProject() {
-    grant(local, OWNER, admin, "refs/*");
+  private static void assertOwner(String ref, ProjectControl u) {
+    assertTrue("OWN " + ref, u.controlForRef(ref).isOwner());
+  }
 
-    ProjectControl uBlah = user(devs);
-    ProjectControl uAdmin = user(devs, admin);
+  private static void assertNotOwner(String ref, ProjectControl u) {
+    assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
+  }
+
+  private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
+  private Project.NameKey localKey = new Project.NameKey("local");
+  private ProjectConfig local;
+  private final Util util;
+
+  public RefControlTest() {
+    util = new Util();
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    local = new ProjectConfig(localKey);
+    local.createInMemory();
+    util.add(local);
+  }
+
+  public void testOwnerProject() {
+    grant(local, OWNER, ADMIN, "refs/*");
+
+    ProjectControl uBlah = util.user(local, DEVS);
+    ProjectControl uAdmin = util.user(local, DEVS, ADMIN);
 
     assertFalse("not owner", uBlah.isOwner());
     assertTrue("is owner", uAdmin.isOwner());
   }
 
   public void testBranchDelegation1() {
-    grant(local, OWNER, admin, "refs/*");
-    grant(local, OWNER, devs, "refs/heads/x/*");
+    grant(local, OWNER, ADMIN, "refs/*");
+    grant(local, OWNER, DEVS, "refs/heads/x/*");
 
-    ProjectControl uDev = user(devs);
+    ProjectControl uDev = util.user(local, DEVS);
     assertFalse("not owner", uDev.isOwner());
     assertTrue("owns ref", uDev.isOwnerAnyRef());
 
@@ -85,12 +89,12 @@
   }
 
   public void testBranchDelegation2() {
-    grant(local, OWNER, admin, "refs/*");
-    grant(local, OWNER, devs, "refs/heads/x/*");
+    grant(local, OWNER, ADMIN, "refs/*");
+    grant(local, OWNER, DEVS, "refs/heads/x/*");
     grant(local, OWNER, fixers, "refs/heads/x/y/*");
     doNotInherit(local, OWNER, "refs/heads/x/y/*");
 
-    ProjectControl uDev = user(devs);
+    ProjectControl uDev = util.user(local, DEVS);
     assertFalse("not owner", uDev.isOwner());
     assertTrue("owns ref", uDev.isOwnerAnyRef());
 
@@ -100,7 +104,7 @@
     assertNotOwner("refs/*", uDev);
     assertNotOwner("refs/heads/master", uDev);
 
-    ProjectControl uFix = user(fixers);
+    ProjectControl uFix = util.user(local, fixers);
     assertFalse("not owner", uFix.isOwner());
     assertTrue("owns ref", uFix.isOwnerAnyRef());
 
@@ -113,13 +117,13 @@
   }
 
   public void testInheritRead_SingleBranchDeniesUpload() {
-    grant(parent, READ, registered, "refs/*");
-    grant(parent, PUSH, registered, "refs/for/refs/*");
-    grant(local, READ, registered, "refs/heads/foobar");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(util.getParentConfig(), PUSH, REGISTERED, "refs/for/refs/*");
+    grant(local, READ, REGISTERED, "refs/heads/foobar");
     doNotInherit(local, READ, "refs/heads/foobar");
     doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
 
     assertTrue("can upload refs/heads/master", //
@@ -130,11 +134,11 @@
   }
 
   public void testInheritRead_SingleBranchDoesNotOverrideInherited() {
-    grant(parent, READ, registered, "refs/*");
-    grant(parent, PUSH, registered, "refs/for/refs/*");
-    grant(local, READ, registered, "refs/heads/foobar");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(util.getParentConfig(), PUSH, REGISTERED, "refs/for/refs/*");
+    grant(local, READ, REGISTERED, "refs/heads/foobar");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
 
     assertTrue("can upload refs/heads/master", //
@@ -145,30 +149,30 @@
   }
 
   public void testInheritDuplicateSections() {
-    grant(parent, READ, admin, "refs/*");
-    grant(local, READ, devs, "refs/heads/*");
-    local.getProject().setParentName(parent.getProject().getName());
-    assertTrue("a can read", user("a", admin).isVisible());
+    grant(util.getParentConfig(), READ, ADMIN, "refs/*");
+    grant(local, READ, DEVS, "refs/heads/*");
+    local.getProject().setParentName(util.getParentConfig().getProject().getName());
+    assertTrue("a can read", util.user(local, "a", ADMIN).isVisible());
 
     local = new ProjectConfig(new Project.NameKey("local"));
     local.createInMemory();
-    grant(local, READ, devs, "refs/*");
-    assertTrue("d can read", user("d", devs).isVisible());
+    grant(local, READ, DEVS, "refs/*");
+    assertTrue("d can read", util.user(local, "d", DEVS).isVisible());
   }
 
   public void testInheritRead_OverrideWithDeny() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, registered, "refs/*").setDeny();
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, REGISTERED, "refs/*").setDeny();
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertFalse("can't read", u.isVisible());
   }
 
   public void testInheritRead_AppendWithDenyOfRef() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, registered, "refs/heads/*").setDeny();
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, REGISTERED, "refs/heads/*").setDeny();
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can read", u.isVisible());
     assertTrue("can read", u.controlForRef("refs/master").isVisible());
     assertTrue("can read", u.controlForRef("refs/tags/foobar").isVisible());
@@ -176,11 +180,11 @@
   }
 
   public void testInheritRead_OverridesAndDeniesOfRef() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, registered, "refs/*").setDeny();
-    grant(local, READ, registered, "refs/heads/*");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, REGISTERED, "refs/*").setDeny();
+    grant(local, READ, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can read", u.isVisible());
     assertFalse("can't read", u.controlForRef("refs/foobar").isVisible());
     assertFalse("can't read", u.controlForRef("refs/tags/foobar").isVisible());
@@ -188,65 +192,65 @@
   }
 
   public void testInheritSubmit_OverridesAndDeniesOfRef() {
-    grant(parent, SUBMIT, registered, "refs/*");
-    grant(local, SUBMIT, registered, "refs/*").setDeny();
-    grant(local, SUBMIT, registered, "refs/heads/*");
+    grant(util.getParentConfig(), SUBMIT, REGISTERED, "refs/*");
+    grant(local, SUBMIT, REGISTERED, "refs/*").setDeny();
+    grant(local, SUBMIT, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit());
     assertFalse("can't submit", u.controlForRef("refs/tags/foobar").canSubmit());
     assertTrue("can submit", u.controlForRef("refs/heads/foobar").canSubmit());
   }
 
   public void testCannotUploadToAnyRef() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, devs, "refs/heads/*");
-    grant(local, PUSH, devs, "refs/for/refs/heads/*");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, DEVS, "refs/heads/*");
+    grant(local, PUSH, DEVS, "refs/for/refs/heads/*");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertFalse("cannot upload", u.canPushToAtLeastOneRef() == Capable.OK);
     assertFalse("cannot upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
   }
 
   public void testUsernamePatternNonRegex() {
-    grant(local, READ, devs, "refs/sb/${username}/heads/*");
+    grant(local, READ, DEVS, "refs/sb/${username}/heads/*");
 
-    ProjectControl u = user("u", devs), d = user("d", devs);
+    ProjectControl u = util.user(local, "u", DEVS), d = util.user(local, "d", DEVS);
     assertFalse("u can't read", u.controlForRef("refs/sb/d/heads/foobar").isVisible());
     assertTrue("d can read", d.controlForRef("refs/sb/d/heads/foobar").isVisible());
   }
 
   public void testUsernamePatternWithRegex() {
-    grant(local, READ, devs, "^refs/sb/${username}/heads/.*");
+    grant(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
 
-    ProjectControl u = user("d.v", devs), d = user("dev", devs);
+    ProjectControl u = util.user(local, "d.v", DEVS), d = util.user(local, "dev", DEVS);
     assertFalse("u can't read", u.controlForRef("refs/sb/dev/heads/foobar").isVisible());
     assertTrue("d can read", d.controlForRef("refs/sb/dev/heads/foobar").isVisible());
   }
 
   public void testSortWithRegex() {
-    grant(local, READ, devs, "^refs/heads/.*");
-    grant(parent, READ, anonymous, "^refs/heads/.*-QA-.*");
+    grant(local, READ, DEVS, "^refs/heads/.*");
+    grant(util.getParentConfig(), READ, ANONYMOUS, "^refs/heads/.*-QA-.*");
 
-    ProjectControl u = user(devs), d = user(devs);
+    ProjectControl u = util.user(local, DEVS), d = util.user(local, DEVS);
     assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
     assertTrue("d can read", d.controlForRef("refs/heads/foo-QA-bar").isVisible());
   }
 
   public void testBlockRule_ParentBlocksChild() {
-    grant(local, PUSH, devs, "refs/tags/*");
-    grant(parent, PUSH, anonymous, "refs/tags/*").setBlock();
+    grant(local, PUSH, DEVS, "refs/tags/*");
+    grant(util.getParentConfig(), PUSH, ANONYMOUS, "refs/tags/*").setBlock();
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't force update tag", u.controlForRef("refs/tags/V10").canForceUpdate());
   }
 
   public void testBlockLabelRange_ParentBlocksChild() {
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
-    grant(parent, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    grant(util.getParentConfig(), LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*").setBlock();
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertTrue("u can vote -1", range.contains(-1));
@@ -256,329 +260,140 @@
   }
 
   public void testUnblockNoForce() {
-    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
-    grant(local, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, PUSH, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertTrue("u can push", u.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockForce() {
-    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    PermissionRule r = grant(local, PUSH, ANONYMOUS, "refs/heads/*");
     r.setBlock();
     r.setForce(true);
-    grant(local, PUSH, devs, "refs/heads/*").setForce(true);
+    grant(local, PUSH, DEVS, "refs/heads/*").setForce(true);
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertTrue("u can force push", u.controlForRef("refs/heads/master").canForceUpdate());
   }
 
   public void testUnblockForceWithAllowNoForce_NotPossible() {
-    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    PermissionRule r = grant(local, PUSH, ANONYMOUS, "refs/heads/*");
     r.setBlock();
     r.setForce(true);
-    grant(local, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't force push", u.controlForRef("refs/heads/master").canForceUpdate());
   }
 
   public void testUnblockMoreSpecificRef_Fails() {
-    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
-    grant(local, PUSH, devs, "refs/heads/master");
+    grant(local, PUSH, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, PUSH, DEVS, "refs/heads/master");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockLargerScope_Fails() {
-    grant(local, PUSH, anonymous, "refs/heads/master").setBlock();
-    grant(local, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, ANONYMOUS, "refs/heads/master").setBlock();
+    grant(local, PUSH, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockInLocal_Fails() {
-    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(util.getParentConfig(), PUSH, ANONYMOUS, "refs/heads/*").setBlock();
     grant(local, PUSH, fixers, "refs/heads/*");
 
-    ProjectControl f = user(fixers);
+    ProjectControl f = util.user(local, fixers);
     assertFalse("u can't push", f.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockInParentBlockInLocal() {
-    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
-    grant(parent, PUSH, devs, "refs/heads/*");
-    grant(local, PUSH, devs, "refs/heads/*").setBlock();
+    grant(util.getParentConfig(), PUSH, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(util.getParentConfig(), PUSH, DEVS, "refs/heads/*");
+    grant(local, PUSH, DEVS, "refs/heads/*").setBlock();
 
-    ProjectControl d = user(devs);
+    ProjectControl d = util.user(local, DEVS);
     assertFalse("u can't push", d.controlForRef("refs/heads/master").canUpdate());
   }
 
-  public void testUnblockVisibilityByRegisteredUsers() {
-    grant(local, READ, anonymous, "refs/heads/*").setBlock();
-    grant(local, READ, registered, "refs/heads/*");
+  public void testUnblockVisibilityByREGISTEREDUsers() {
+    grant(local, READ, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, READ, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user(registered);
+    ProjectControl u = util.user(local, REGISTERED);
     assertTrue("u can read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
   }
 
   public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() {
-    grant(parent, READ, anonymous, "refs/heads/*").setBlock();
-    grant(local, READ, registered, "refs/heads/*");
+    grant(util.getParentConfig(), READ, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, READ, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user(registered);
+    ProjectControl u = util.user(local, REGISTERED);
     assertFalse("u can't read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
   }
 
   public void testUnblockForceEditTopicName() {
-    grant(local, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
-    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+    grant(local, EDIT_TOPIC_NAME, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
-    ProjectControl u = user(devs);
-    assertTrue("u can edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+    ProjectControl u = util.user(local, DEVS);
+    assertTrue("u can edit topic name", u.controlForRef("refs/heads/master")
+        .canForceEditTopicName());
   }
 
   public void testUnblockInLocalForceEditTopicName_Fails() {
-    grant(parent, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
-    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+    grant(util.getParentConfig(), EDIT_TOPIC_NAME, ANONYMOUS, "refs/heads/*")
+        .setBlock();
+    grant(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
-    ProjectControl u = user(registered);
-    assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+    ProjectControl u = util.user(local, REGISTERED);
+    assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master")
+        .canForceEditTopicName());
   }
 
   public void testUnblockRange() {
-    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertTrue("u can vote -2", range.contains(-2));
     assertTrue("u can vote +2", range.contains(2));
   }
 
   public void testUnblockRangeOnMoreSpecificRef_Fails() {
-    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/master");
+    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/master");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote +2", range.contains(-2));
   }
 
   public void testUnblockRangeOnLargerScope_Fails() {
-    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/master").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS, "refs/heads/master").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote +2", range.contains(-2));
   }
 
   public void testUnblockInLocalRange_Fails() {
-    grant(parent, LABEL + "Code-Review", -1, 1, anonymous, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+    grant(util.getParentConfig(), LABEL + "Code-Review", -1, 1, ANONYMOUS,
+        "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
-    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    ProjectControl u = util.user(local, DEVS);
+    PermissionRange range =
+        u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote 2", range.contains(2));
   }
-  // -----------------------------------------------------------------------
-
-  private final Map<Project.NameKey, ProjectState> all;
-  private final AllProjectsName allProjectsName = new AllProjectsName("parent");
-  private final ProjectCache projectCache;
-
-  private ProjectConfig local;
-  private ProjectConfig parent;
-  private PermissionCollection.Factory sectionSorter;
-
-  private final AccountGroup.UUID admin = new AccountGroup.UUID("test.admin");
-  private final AccountGroup.UUID anonymous = AccountGroup.ANONYMOUS_USERS;
-  private final AccountGroup.UUID registered = AccountGroup.REGISTERED_USERS;
-
-  private final AccountGroup.UUID devs = new AccountGroup.UUID("test.devs");
-  private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
-
-  private final CapabilityControl.Factory capabilityControlFactory;
-
-  public RefControlTest() {
-    all = new HashMap<Project.NameKey, ProjectState>();
-    projectCache = new ProjectCache() {
-      @Override
-      public ProjectState getAllProjects() {
-        return get(allProjectsName);
-      }
-
-      @Override
-      public ProjectState get(Project.NameKey projectName) {
-        return all.get(projectName);
-      }
-
-      @Override
-      public void evict(Project p) {
-      }
-
-      @Override
-      public void remove(Project p) {
-      }
-
-      @Override
-      public Iterable<Project.NameKey> all() {
-        return Collections.emptySet();
-      }
-
-      @Override
-      public Iterable<Project.NameKey> byName(String prefix) {
-        return Collections.emptySet();
-      }
-
-      @Override
-      public void onCreateProject(Project.NameKey newProjectName) {
-      }
-
-      @Override
-      public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-        return Collections.emptySet();
-      }
-    };
-
-    Injector injector = Guice.createInjector(new FactoryModule() {
-      @Override
-      protected void configure() {
-        bind(Config.class)
-            .annotatedWith(GerritServerConfig.class)
-            .toInstance(new Config());
-
-        factory(CapabilityControl.Factory.class);
-        bind(ProjectCache.class).toInstance(projectCache);
-      }
-    });
-    capabilityControlFactory = injector.getInstance(CapabilityControl.Factory.class);
-  }
-
-  @Override
-  public void setUp() throws Exception {
-    super.setUp();
-
-    parent = new ProjectConfig(new Project.NameKey("parent"));
-    parent.createInMemory();
-
-    local = new ProjectConfig(new Project.NameKey("local"));
-    local.createInMemory();
-
-    Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
-        CacheBuilder.newBuilder().build();
-    sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c));
-  }
-
-  private static void assertOwner(String ref, ProjectControl u) {
-    assertTrue("OWN " + ref, u.controlForRef(ref).isOwner());
-  }
-
-  private static void assertNotOwner(String ref, ProjectControl u) {
-    assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
-  }
-
-  private PermissionRule grant(ProjectConfig project, String permissionName,
-      AccountGroup.UUID group, String ref) {
-    return grant(project, permissionName, newRule(project, group), ref);
-  }
-
-  private PermissionRule grant(ProjectConfig project, String permissionName,
-      int min, int max, AccountGroup.UUID group, String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    return grant(project, permissionName, rule, ref);
-  }
-
-
-  private PermissionRule grant(ProjectConfig project, String permissionName,
-      PermissionRule rule, String ref) {
-    project.getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .add(rule);
-    return rule;
-  }
-
-  private void doNotInherit(ProjectConfig project, String permissionName,
-      String ref) {
-    project.getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .setExclusiveGroup(true);
-  }
-
-  private PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
-    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
-    group = project.resolve(group);
-
-    return new PermissionRule(group);
-  }
-
-  private ProjectControl user(AccountGroup.UUID... memberOf) {
-    return user(null, memberOf);
-  }
-
-  private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
-    String canonicalWebUrl = "http://localhost";
-
-    return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
-        Collections.<AccountGroup.UUID> emptySet(), projectCache,
-        sectionSorter, null,
-        canonicalWebUrl, new MockUser(name, memberOf),
-        newProjectState());
-  }
-
-  private ProjectState newProjectState() {
-    PrologEnvironment.Factory envFactory = null;
-    GitRepositoryManager mgr = null;
-    ProjectControl.AssistedFactory projectControlFactory = null;
-    RulesCache rulesCache = null;
-    all.put(local.getProject().getNameKey(), new ProjectState(
-        null, projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, null, local));
-    all.put(parent.getProject().getNameKey(), new ProjectState(
-        null, projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, null, parent));
-    return all.get(local.getProject().getNameKey());
-  }
-
-  private class MockUser extends CurrentUser {
-    private final String username;
-    private final GroupMembership groups;
-
-    MockUser(String name, AccountGroup.UUID[] groupId) {
-      super(RefControlTest.this.capabilityControlFactory);
-      username = name;
-      ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
-      groupIds.add(registered);
-      groupIds.add(anonymous);
-      groups = new ListGroupMembership(groupIds);
-    }
-
-    @Override
-    public GroupMembership getEffectiveGroups() {
-      return groups;
-    }
-
-    @Override
-    public String getUserName() {
-      return username;
-    }
-
-    @Override
-    public Set<Change.Id> getStarredChanges() {
-      return Collections.emptySet();
-    }
-
-    @Override
-    public Collection<AccountProjectWatch> getNotificationFilters() {
-      return Collections.emptySet();
-    }
-  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
new file mode 100644
index 0000000..a99eba1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -0,0 +1,264 @@
+// 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.project;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.RulesCache;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class Util {
+  public static AccountGroup.UUID ANONYMOUS = AccountGroup.ANONYMOUS_USERS;
+  public static AccountGroup.UUID REGISTERED = AccountGroup.REGISTERED_USERS;
+  public static AccountGroup.UUID ADMIN = new AccountGroup.UUID("test.admin");
+  public static AccountGroup.UUID DEVS = new AccountGroup.UUID("test.devs");
+
+  public static LabelType CR = category("Code-Review",
+      value(2, "Looks good to me, approved"),
+      value(1, "Looks good to me, but someone else must approve"),
+      value(0, "No score"),
+      value(-1, "I would prefer that you didn't submit this"),
+      value(-2, "Do not submit"));
+
+  public static LabelValue value(int value, String text) {
+    return new LabelValue((short) value, text);
+  }
+
+  public static LabelType category(String name, LabelValue... values) {
+    return new LabelType(name, Arrays.asList(values));
+  }
+
+  static public PermissionRule newRule(ProjectConfig project,
+      AccountGroup.UUID groupUUID) {
+    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
+    group = project.resolve(group);
+
+    return new PermissionRule(group);
+  }
+
+  static public PermissionRule grant(ProjectConfig project,
+      String permissionName, int min, int max, AccountGroup.UUID group,
+      String ref) {
+    PermissionRule rule = newRule(project, group);
+    rule.setMin(min);
+    rule.setMax(max);
+    return grant(project, permissionName, rule, ref);
+  }
+
+  static public PermissionRule grant(ProjectConfig project,
+      String permissionName, AccountGroup.UUID group, String ref) {
+    return grant(project, permissionName, newRule(project, group), ref);
+  }
+
+  static public void doNotInherit(ProjectConfig project, String permissionName,
+      String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .setExclusiveGroup(true);
+  }
+
+  static private PermissionRule grant(ProjectConfig project,
+      String permissionName, PermissionRule rule, String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .add(rule);
+    return rule;
+  }
+
+  private final Map<Project.NameKey, ProjectState> all;
+  private final ProjectCache projectCache;
+  private final CapabilityControl.Factory capabilityControlFactory;
+  private final PermissionCollection.Factory sectionSorter;
+
+  private final AllProjectsName allProjectsName = new AllProjectsName("parent");
+  private final ProjectConfig parent = new ProjectConfig(allProjectsName);
+
+  public Util() {
+    all = new HashMap<Project.NameKey, ProjectState>();
+    parent.createInMemory();
+    parent.getLabelSections().put(CR.getName(), CR);
+
+    add(parent);
+
+    projectCache = new ProjectCache() {
+      @Override
+      public ProjectState getAllProjects() {
+        return get(allProjectsName);
+      }
+
+      @Override
+      public ProjectState get(Project.NameKey projectName) {
+        return all.get(projectName);
+      }
+
+      @Override
+      public void evict(Project p) {
+      }
+
+      @Override
+      public void remove(Project p) {
+      }
+
+      @Override
+      public Iterable<Project.NameKey> all() {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public Iterable<Project.NameKey> byName(String prefix) {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public void onCreateProject(Project.NameKey newProjectName) {
+      }
+
+      @Override
+      public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public ProjectState checkedGet(NameKey projectName) throws IOException {
+        return all.get(projectName);
+      }
+
+      @Override
+      public void evict(NameKey p) {
+      }
+    };
+
+    Injector injector = Guice.createInjector(new FactoryModule() {
+      @Override
+      protected void configure() {
+        bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(
+            new Config());
+
+        factory(CapabilityControl.Factory.class);
+        bind(ProjectCache.class).toInstance(projectCache);
+      }
+    });
+
+    Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
+        CacheBuilder.newBuilder().build();
+    sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c));
+    capabilityControlFactory =
+        injector.getInstance(CapabilityControl.Factory.class);
+  }
+
+  public ProjectConfig getParentConfig() {
+    return this.parent;
+  }
+
+  public void add(ProjectConfig pc) {
+    PrologEnvironment.Factory envFactory = null;
+    GitRepositoryManager mgr = null;
+    ProjectControl.AssistedFactory projectControlFactory = null;
+    RulesCache rulesCache = null;
+    SitePaths sitePaths = null;
+    List<CommentLinkInfo> commentLinks = null;
+
+    all.put(pc.getProject().getNameKey(), new ProjectState(sitePaths,
+        projectCache, allProjectsName, projectControlFactory, envFactory, mgr,
+        rulesCache, commentLinks, pc));
+  }
+
+  public ProjectControl user(ProjectConfig local, AccountGroup.UUID... memberOf) {
+    return user(local, null, memberOf);
+  }
+
+  public ProjectControl user(ProjectConfig local, String name,
+      AccountGroup.UUID... memberOf) {
+    String canonicalWebUrl = "http://localhost";
+
+    return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
+        Collections.<AccountGroup.UUID> emptySet(), projectCache,
+        sectionSorter, null, canonicalWebUrl, new MockUser(name, memberOf),
+        newProjectState(local));
+  }
+
+  private ProjectState newProjectState(ProjectConfig local) {
+    add(local);
+    return all.get(local.getProject().getNameKey());
+  }
+
+  private class MockUser extends CurrentUser {
+    private final String username;
+    private final GroupMembership groups;
+
+    MockUser(String name, AccountGroup.UUID[] groupId) {
+      super(capabilityControlFactory);
+      username = name;
+      ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
+      groupIds.add(REGISTERED);
+      groupIds.add(ANONYMOUS);
+      groups = new ListGroupMembership(groupIds);
+    }
+
+    @Override
+    public GroupMembership getEffectiveGroups() {
+      return groups;
+    }
+
+    @Override
+    public String getUserName() {
+      return username;
+    }
+
+    @Override
+    public Set<Change.Id> getStarredChanges() {
+      return Collections.emptySet();
+    }
+
+    @Override
+    public Collection<AccountProjectWatch> getNotificationFilters() {
+      return Collections.emptySet();
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractIndexQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractIndexQueryChangesTest.java
new file mode 100644
index 0000000..5dacd49
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractIndexQueryChangesTest.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.project.ChangeControl;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.List;
+
+@Ignore
+public abstract class AbstractIndexQueryChangesTest
+    extends AbstractQueryChangesTest {
+  @Test
+  public void byFileExact() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    RevCommit commit = repo.parseBody(
+        repo.commit().message("one")
+        .add("file1", "contents1").add("file2", "contents2")
+        .create());
+    Change change = newChange(repo, commit, null, null, null).insert();
+
+    assertTrue(query("file:file").isEmpty());
+    assertResultEquals(change, queryOne("file:file1"));
+    assertResultEquals(change, queryOne("file:file2"));
+  }
+
+  @Test
+  public void byFileRegex() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    RevCommit commit = repo.parseBody(
+        repo.commit().message("one")
+        .add("file1", "contents1").add("file2", "contents2")
+        .create());
+    Change change = newChange(repo, commit, null, null, null).insert();
+
+    assertTrue(query("file:file.*").isEmpty());
+    assertResultEquals(change, queryOne("file:^file.*"));
+  }
+
+  @Test
+  public void byComment() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins = newChange(repo, null, null, null, null);
+    Change change = ins.insert();
+    ChangeControl ctl = changeControlFactory.controlFor(change, user);
+
+    PostReview.Input input = new PostReview.Input();
+    input.message = "toplevel";
+    PostReview.Comment comment = new PostReview.Comment();
+    comment.line = 1;
+    comment.message = "inline";
+    input.comments = ImmutableMap.<String, List<PostReview.Comment>> of(
+        "Foo.java", ImmutableList.<PostReview.Comment> of(comment));
+    postReview.apply(new RevisionResource(
+        new ChangeResource(ctl), ins.getPatchSet()), input);
+
+    assertTrue(query("comment:foo").isEmpty());
+    assertResultEquals(change, queryOne("comment:toplevel"));
+    assertResultEquals(change, queryOne("comment:inline"));
+  }
+}
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
new file mode 100644
index 0000000..1e8b87a
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -0,0 +1,663 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.joda.time.DateTimeUtils;
+import org.joda.time.DateTimeUtils.MillisProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Ignore
+public abstract class AbstractQueryChangesTest {
+  private static final TopLevelResource TLR = TopLevelResource.INSTANCE;
+
+  @Inject protected AccountManager accountManager;
+  @Inject protected ChangeControl.GenericFactory changeControlFactory;
+  @Inject protected ChangeInserter.Factory changeFactory;
+  @Inject protected CreateProject.Factory projectFactory;
+  @Inject protected IdentifiedUser.RequestFactory userFactory;
+  @Inject protected InMemoryDatabase schemaFactory;
+  @Inject protected InMemoryRepositoryManager repoManager;
+  @Inject protected PostReview postReview;
+  @Inject protected ProjectControl.GenericFactory projectControlFactory;
+  @Inject protected Provider<QueryChanges> queryProvider;
+  @Inject protected SchemaCreator schemaCreator;
+  @Inject protected ThreadLocalRequestContext requestContext;
+
+  protected LifecycleManager lifecycle;
+  protected ReviewDb db;
+  protected Account.Id userId;
+  protected CurrentUser user;
+  protected volatile long clockStepMs;
+
+  protected abstract Injector createInjector();
+
+  @Before
+  public void setUpInjector() throws Exception {
+    Injector injector = createInjector();
+    injector.injectMembers(this);
+    lifecycle = new LifecycleManager();
+    lifecycle.add(injector);
+    lifecycle.start();
+
+    db = schemaFactory.open();
+    schemaCreator.create(db);
+    userId = accountManager.authenticate(AuthRequest.forUser("user"))
+        .getAccountId();
+    user = userFactory.create(userId);
+
+    requestContext.setContext(new RequestContext() {
+      @Override
+      public CurrentUser getCurrentUser() {
+        return user;
+      }
+
+      @Override
+      public Provider<ReviewDb> getReviewDbProvider() {
+        return Providers.of(db);
+      }
+    });
+  }
+
+  @After
+  public void tearDownInjector() {
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
+    requestContext.setContext(null);
+    if (db != null) {
+      db.close();
+    }
+    InMemoryDatabase.drop(schemaFactory);
+  }
+
+  @Before
+  public void setMillisProvider() {
+    clockStepMs = 1;
+    final AtomicLong clockMs = new AtomicLong(
+        MILLISECONDS.convert(ChangeUtil.SORT_KEY_EPOCH_MINS, MINUTES)
+        + MILLISECONDS.convert(60, DAYS));
+
+    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
+      @Override
+      public long getMillis() {
+        return clockMs.getAndAdd(clockStepMs);
+      }
+    });
+  }
+
+  @After
+  public void resetMillisProvider() {
+    DateTimeUtils.setCurrentMillisSystem();
+  }
+
+  @Test
+  public void byId() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    assertTrue(query("12345").isEmpty());
+    assertResultEquals(change1, queryOne(change1.getId().get()));
+    assertResultEquals(change2, queryOne(change2.getId().get()));
+  }
+
+  @Test
+  public void byKey() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change = newChange(repo, null, null, null, null).insert();
+    String key = change.getKey().get();
+
+    assertTrue(query("I0000000000000000000000000000000000000000").isEmpty());
+    for (int i = 0; i <= 36; i++) {
+      String q = key.substring(0, 41 - i);
+      assertResultEquals("result for " + q, change, queryOne(q));
+    }
+  }
+
+  @Test
+  public void byStatus() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.getChange();
+    change1.setStatus(Change.Status.NEW);
+    ins1.insert();
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.getChange();
+    change2.setStatus(Change.Status.MERGED);
+    ins2.insert();
+
+    assertResultEquals(change1, queryOne("status:new"));
+    assertResultEquals(change1, queryOne("is:new"));
+    assertResultEquals(change2, queryOne("status:merged"));
+    assertResultEquals(change2, queryOne("is:merged"));
+  }
+
+  @Test
+  public void byStatusOpen() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.getChange();
+    change1.setStatus(Change.Status.NEW);
+    ins1.insert();
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.getChange();
+    change2.setStatus(Change.Status.DRAFT);
+    ins2.insert();
+    ChangeInserter ins3 = newChange(repo, null, null, null, null);
+    Change change3 = ins3.getChange();
+    change3.setStatus(Change.Status.MERGED);
+    ins3.insert();
+
+    List<ChangeInfo> results;
+    results = query("status:open");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+    results = query("is:open");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void byStatusClosed() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.getChange();
+    change1.setStatus(Change.Status.MERGED);
+    ins1.insert();
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.getChange();
+    change2.setStatus(Change.Status.ABANDONED);
+    ins2.insert();
+    ChangeInserter ins3 = newChange(repo, null, null, null, null);
+    Change change3 = ins3.getChange();
+    change3.setStatus(Change.Status.NEW);
+    ins3.insert();
+
+    List<ChangeInfo> results;
+    results = query("status:closed");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+    results = query("is:closed");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void byCommit() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins = newChange(repo, null, null, null, null);
+    ins.insert();
+    String sha = ins.getPatchSet().getRevision().get();
+
+    assertTrue(query("0000000000000000000000000000000000000000").isEmpty());
+    for (int i = 0; i <= 36; i++) {
+      String q = sha.substring(0, 40 - i);
+      assertResultEquals("result for " + q, ins.getChange(), queryOne(q));
+    }
+  }
+
+  @Test
+  public void byOwner() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    Change change2 = newChange(repo, null, null, user2, null).insert();
+
+    assertResultEquals(change1, queryOne("owner:" + userId.get()));
+    assertResultEquals(change2, queryOne("owner:" + user2));
+  }
+
+  @Test
+  public void byOwnerIn() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    Change change2 = newChange(repo, null, null, user2, null).insert();
+
+    assertResultEquals(change1, queryOne("ownerin:Administrators"));
+    List<ChangeInfo> results = query("ownerin:\"Registered Users\"");
+    assertEquals(results.toString(), 2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void byProject() throws Exception {
+    TestRepository<InMemoryRepository> repo1 = createProject("repo1");
+    TestRepository<InMemoryRepository> repo2 = createProject("repo2");
+    Change change1 = newChange(repo1, null, null, null, null).insert();
+    Change change2 = newChange(repo2, null, null, null, null).insert();
+
+    assertTrue(query("project:foo").isEmpty());
+    assertResultEquals(change1, queryOne("project:repo1"));
+    assertResultEquals(change2, queryOne("project:repo2"));
+  }
+
+  @Test
+  public void byBranchAndRef() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, "master").insert();
+    Change change2 = newChange(repo, null, null, null, "branch").insert();
+
+    assertTrue(query("branch:foo").isEmpty());
+    assertResultEquals(change1, queryOne("branch:master"));
+    assertResultEquals(change1, queryOne("branch:refs/heads/master"));
+    assertTrue(query("ref:master").isEmpty());
+    assertResultEquals(change1, queryOne("ref:refs/heads/master"));
+    assertResultEquals(change1, queryOne("branch:refs/heads/master"));
+    assertResultEquals(change2, queryOne("branch:branch"));
+    assertResultEquals(change2, queryOne("branch:refs/heads/branch"));
+    assertTrue(query("ref:branch").isEmpty());
+    assertResultEquals(change2, queryOne("ref:refs/heads/branch"));
+  }
+
+  @Test
+  public void byTopic() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.getChange();
+    change1.setTopic("feature1");
+    ins1.insert();
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.getChange();
+    change2.setTopic("feature2");
+    ins2.insert();
+
+    assertTrue(query("topic:foo").isEmpty());
+    assertResultEquals(change1, queryOne("topic:feature1"));
+    assertResultEquals(change2, queryOne("topic:feature2"));
+  }
+
+  @Test
+  public void byMessageExact() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
+    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
+    Change change2 = newChange(repo, commit2, null, null, null).insert();
+
+    assertTrue(query("topic:foo").isEmpty());
+    assertResultEquals(change1, queryOne("message:one"));
+    assertResultEquals(change2, queryOne("message:two"));
+  }
+
+  @Test
+  public void byLabel() throws Exception {
+    accountManager.authenticate(AuthRequest.forUser("anotheruser"));
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins = newChange(repo, null, null, null, null);
+    Change change = ins.insert();
+    ChangeControl ctl = changeControlFactory.controlFor(change, user);
+
+    PostReview.Input input = new PostReview.Input();
+    input.message = "toplevel";
+    input.labels = ImmutableMap.<String, Short> of("Code-Review", (short) 1);
+    postReview.apply(new RevisionResource(
+        new ChangeResource(ctl), ins.getPatchSet()), input);
+
+    assertTrue(query("label:Code-Review=-2").isEmpty());
+    assertTrue(query("label:Code-Review-2").isEmpty());
+    assertTrue(query("label:Code-Review=-1").isEmpty());
+    assertTrue(query("label:Code-Review-1").isEmpty());
+    assertTrue(query("label:Code-Review=0").isEmpty());
+    assertResultEquals(change, queryOne("label:Code-Review=+1"));
+    assertResultEquals(change, queryOne("label:Code-Review=1"));
+    assertResultEquals(change, queryOne("label:Code-Review+1"));
+    assertTrue(query("label:Code-Review=+2").isEmpty());
+    assertTrue(query("label:Code-Review=2").isEmpty());
+    assertTrue(query("label:Code-Review+2").isEmpty());
+
+    // TODO(dborowitz): > and < are broken at head.
+
+    assertResultEquals(change, queryOne("label:Code-Review>=0"));
+    //assertResultEquals(change, queryOne("label:Code-Review>0"));
+    assertResultEquals(change, queryOne("label:Code-Review>=1"));
+    //assertTrue(query("label:Code-Review>1").isEmpty());
+    assertTrue(query("label:Code-Review>=2").isEmpty());
+
+    assertResultEquals(change, queryOne("label: Code-Review<=2"));
+    //assertResultEquals(change, queryOne("label: Code-Review<2"));
+    assertResultEquals(change, queryOne("label: Code-Review<=1"));
+    //assertTrue(query("label: Code-Review<1").isEmpty());
+    assertTrue(query("label: Code-Review<=0").isEmpty());
+
+    assertTrue(query("label:Code-Review=+1,anotheruser").isEmpty());
+    assertResultEquals(change, queryOne("label:Code-Review=+1,user"));
+    assertResultEquals(change, queryOne("label:Code-Review=+1,user=user"));
+    assertResultEquals(change, queryOne("label:Code-Review=+1,Administrators"));
+    assertResultEquals(change, queryOne("label:Code-Review=+1,group=Administrators"));
+  }
+
+  @Test
+  public void limit() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change last = null;
+    int n = 5;
+    for (int i = 0; i < n; i++) {
+      last = newChange(repo, null, null, null, null).insert();
+    }
+
+    List<ChangeInfo> results;
+    for (int i = 1; i <= n + 2; i++) {
+      results = query("status:new limit:" + i);
+      assertEquals(Math.min(i, n), results.size());
+      assertResultEquals(last, results.get(0));
+    }
+  }
+
+  @Test
+  public void pagination() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    List<Change> changes = Lists.newArrayList();
+    for (int i = 0; i < 5; i++) {
+      changes.add(newChange(repo, null, null, null, null).insert());
+    }
+
+    // Page forward and back through 3 pages of results.
+    QueryChanges q;
+    List<ChangeInfo> results;
+    results = query("status:new limit:2");
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(4), results.get(0));
+    assertResultEquals(changes.get(3), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyBefore(results.get(1)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(2), results.get(0));
+    assertResultEquals(changes.get(1), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyBefore(results.get(1)._sortkey);
+    results = query(q);
+    assertEquals(1, results.size());
+    assertResultEquals(changes.get(0), results.get(0));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyAfter(results.get(0)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(2), results.get(0));
+    assertResultEquals(changes.get(1), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyAfter(results.get(0)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(4), results.get(0));
+    assertResultEquals(changes.get(3), results.get(1));
+  }
+
+  @Test
+  public void sortKeyWithMinuteResolution() throws Exception {
+    clockStepMs = MILLISECONDS.convert(2, MINUTES);
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.insert();
+    ChangeControl ctl1 = changeControlFactory.controlFor(change1, user);
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
+
+    List<ChangeInfo> results;
+    results = query("status:new");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    PostReview.Input input = new PostReview.Input();
+    input.message = "toplevel";
+    postReview.apply(new RevisionResource(
+        new ChangeResource(ctl1), ins1.getPatchSet()), input);
+    change1 = db.changes().get(change1.getId());
+
+    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
+    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
+        > MILLISECONDS.convert(1, MINUTES));
+
+    results = query("status:new");
+    assertEquals(2, results.size());
+    // change1 moved to the top.
+    assertResultEquals(change1, results.get(0));
+    assertResultEquals(change2, results.get(1));
+  }
+
+  @Test
+  public void sortKeyWithSubMinuteResolution() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.insert();
+    ChangeControl ctl1 = changeControlFactory.controlFor(change1, user);
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
+
+    List<ChangeInfo> results;
+    results = query("status:new");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    PostReview.Input input = new PostReview.Input();
+    input.message = "toplevel";
+    postReview.apply(new RevisionResource(
+        new ChangeResource(ctl1), ins1.getPatchSet()), input);
+    change1 = db.changes().get(change1.getId());
+
+    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
+    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
+        < MILLISECONDS.convert(1, MINUTES));
+
+    results = query("status:new");
+    assertEquals(2, results.size());
+    // Same order as before change1 was modified.
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void sortKeyBreaksTiesOnChangeId() throws Exception {
+    clockStepMs = 0;
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    assertEquals(change1.getLastUpdatedOn(), change2.getLastUpdatedOn());
+
+    List<ChangeInfo> results = query("status:new");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void filterOutMoreThanOnePageOfResults() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change = newChange(repo, null, null, userId.get(), null).insert();
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    for (int i = 0; i < 5; i++) {
+      newChange(repo, null, null, user2, null).insert();
+    }
+
+    assertResultEquals(change, queryOne("status:new ownerin:Administrators"));
+    assertResultEquals(change,
+        queryOne("status:new ownerin:Administrators limit:2"));
+  }
+
+  @Test
+  public void filterOutAllResults() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    for (int i = 0; i < 5; i++) {
+      newChange(repo, null, null, user2, null).insert();
+    }
+
+    assertTrue(query("status:new ownerin:Administrators").isEmpty());
+    assertTrue(query("status:new ownerin:Administrators limit:2").isEmpty());
+  }
+
+  protected ChangeInserter newChange(
+      TestRepository<InMemoryRepository> repo,
+      @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,
+      @Nullable String branch) throws Exception {
+    if (commit == null) {
+      commit = repo.parseBody(repo.commit().message("message").create());
+    }
+    Account.Id ownerId = owner != null ? new Account.Id(owner) : userId;
+    branch = Objects.firstNonNull(branch, "refs/heads/master");
+    if (!branch.startsWith("refs/heads/")) {
+      branch = "refs/heads/" + branch;
+    }
+    Project.NameKey project = new Project.NameKey(
+        repo.getRepository().getDescription().getRepositoryName());
+
+    Change.Id id = new Change.Id(db.nextChangeId());
+    if (key == null) {
+      key = "I" + Hashing.sha1().newHasher()
+          .putInt(id.get())
+          .putString(project.get(), Charsets.UTF_8)
+          .putString(commit.name(), Charsets.UTF_8)
+          .putInt(ownerId.get())
+          .putString(branch, Charsets.UTF_8)
+          .hash()
+          .toString();
+    }
+
+    Change change = new Change(new Change.Key(key), id, ownerId,
+        new Branch.NameKey(project, branch), TimeUtil.nowTs());
+    return changeFactory.create(
+        projectControlFactory.controlFor(project,
+          userFactory.create(ownerId)).controlFor(change).getRefControl(),
+        change,
+        commit);
+  }
+
+  protected void assertResultEquals(Change expected, ChangeInfo actual) {
+    assertEquals(expected.getId().get(), actual._number);
+  }
+
+  protected void assertResultEquals(String message, Change expected,
+      ChangeInfo actual) {
+    assertEquals(message, expected.getId().get(), actual._number);
+  }
+
+  protected TestRepository<InMemoryRepository> createProject(String name)
+      throws Exception {
+    CreateProject create = projectFactory.create(name);
+    create.apply(TLR, new CreateProject.Input());
+    return new TestRepository<InMemoryRepository>(
+        repoManager.openRepository(new Project.NameKey(name)));
+  }
+
+  protected QueryChanges newQuery(Object query) {
+    QueryChanges q = queryProvider.get();
+    q.addQuery(query.toString());
+    return q;
+  }
+
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  protected List<ChangeInfo> query(QueryChanges q) throws Exception {
+    Object result = q.apply(TLR);
+    assertTrue(
+        String.format("expected List<ChangeInfo>, found %s for [%s]",
+          result, q.getQuery(0)),
+        result instanceof List);
+    List results = (List) result;
+    if (!results.isEmpty()) {
+      assertTrue(
+          String.format("expected ChangeInfo, found %s for [%s]",
+            result, q.getQuery(0)),
+          results.get(0) instanceof ChangeInfo);
+    }
+    return (List<ChangeInfo>) result;
+  }
+
+  protected List<ChangeInfo> query(Object query) throws Exception {
+    return query(newQuery(query));
+  }
+
+  protected ChangeInfo queryOne(Object query) throws Exception {
+    List<ChangeInfo> results = query(query);
+    assertTrue(
+        String.format("expected singleton List<ChangeInfo>, found %s for [%s]",
+          results, query),
+        results.size() == 1);
+    return results.get(0);
+  }
+
+  private static long lastUpdatedMs(Change c) {
+    return c.getLastUpdatedOn().getTime();
+  }
+}
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
new file mode 100644
index 0000000..ab12986
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.Config;
+
+public class LuceneQueryChangesTest extends AbstractIndexQueryChangesTest {
+  protected Injector createInjector() {
+    Config cfg = InMemoryModule.newDefaultConfig();
+    cfg.setString("index", null, "type", "lucene");
+    cfg.setBoolean("index", "lucene", "testInmemory", true);
+    cfg.setInt("index", "lucene", "testVersion", 4);
+    return Guice.createInjector(new InMemoryModule(cfg));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
index ce7b25c..1500272 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
@@ -76,7 +76,7 @@
   private static ChangeData change(String... files) {
     Arrays.sort(files);
     ChangeData cd = new ChangeData(new Change.Id(1));
-    cd.setCurrentFilePaths(files);
+    cd.setCurrentFilePaths(Arrays.asList(files));
     return cd;
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/SqlQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/SqlQueryChangesTest.java
new file mode 100644
index 0000000..f235efc
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/SqlQueryChangesTest.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.Config;
+
+public class SqlQueryChangesTest extends AbstractQueryChangesTest {
+  protected Injector createInjector() {
+    Config cfg = InMemoryModule.newDefaultConfig();
+    cfg.setString("index", null, "type", "sql");
+    return Guice.createInjector(new InMemoryModule(cfg));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index b376d11..e58266f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -24,8 +24,10 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryModule;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
 import junit.framework.TestCase;
 
@@ -39,12 +41,19 @@
 import java.util.List;
 
 public class SchemaCreatorTest extends TestCase {
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private GitRepositoryManager repoManager;
+
+  @Inject
   private InMemoryDatabase db;
 
   @Override
   protected void setUp() throws Exception {
     super.setUp();
-    db = new InMemoryDatabase();
+    new InMemoryModule().inject(this);
   }
 
   @Override
@@ -89,10 +98,8 @@
 
   private LabelTypes getLabelTypes() throws Exception {
     db.create();
-    AllProjectsName allProjects = db.getInstance(AllProjectsName.class);
     ProjectConfig c = new ProjectConfig(allProjects);
-    Repository repo = db.getInstance(GitRepositoryManager.class)
-        .openRepository(allProjects);
+    Repository repo = repoManager.openRepository(allProjects);
     try {
       c.load(repo);
       return new LabelTypes(
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index bdd2258..5039cc2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -25,9 +25,9 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryH2Type;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.gwtorm.server.StatementExecutor;
@@ -51,7 +51,7 @@
   @Override
   protected void setUp() throws Exception {
     super.setUp();
-    db = new InMemoryDatabase();
+    db = InMemoryDatabase.newDatabase();
   }
 
   @Override
@@ -74,7 +74,6 @@
         install(new SchemaVersion.Module());
 
         Config cfg = new Config();
-        cfg.setString("gerrit", null, "basePath", "git");
         cfg.setString("user", null, "name", "Gerrit Code Review");
         cfg.setString("user", null, "email", "gerrit@localhost");
 
@@ -89,8 +88,8 @@
         bind(AllProjectsName.class)
             .toInstance(new AllProjectsName("All-Projects"));
 
-        bind(GitRepositoryManager.class) //
-            .to(LocalDiskRepositoryManager.class);
+        bind(GitRepositoryManager.class)
+            .toInstance(new InMemoryRepositoryManager());
 
         bind(String.class) //
           .annotatedWith(AnonymousCowardName.class) //
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index b517de7..47ff6b0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -52,6 +52,8 @@
 
 import static org.junit.Assert.fail;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.io.ByteStreams;
 
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
@@ -64,10 +66,13 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.util.List;
+import java.util.Map;
 
 public abstract class HookTestCase extends LocalDiskRepositoryTestCase {
   protected Repository repository;
-  private File hooksh;
+  private final Map<String, File> hooks = Maps.newTreeMap();
+  private final List<File> cleanup = Lists.newArrayList();
 
   @Override
   @Before
@@ -79,15 +84,21 @@
   @Override
   @After
   public void tearDown() throws Exception {
-    if (hooksh != null) {
-      if (!hooksh.delete()) {
-        hooksh.deleteOnExit();
+    super.tearDown();
+    for (File p : cleanup) {
+      if (!p.delete()) {
+        p.deleteOnExit();
       }
-      hooksh = null;
     }
+    cleanup.clear();
   }
 
   protected File getHook(final String name) throws IOException {
+    File hook = hooks.get(name);
+    if (hook != null) {
+      return hook;
+    }
+
     final String scproot = "com/google/gerrit/server/tools/root";
     final String path = scproot + "/hooks/" + name;
     URL url = cl().getResource(path);
@@ -95,17 +106,22 @@
       fail("Cannot locate " + path + " in CLASSPATH");
     }
 
-    File hook;
     if ("file".equals(url.getProtocol())) {
       hook = new File(url.getPath());
       if (!hook.isFile()) {
         fail("Cannot locate " + path + " in CLASSPATH");
       }
+      long time = hook.lastModified();
+      hook.setExecutable(true);
+      hook.setLastModified(time);
+      hooks.put(name, hook);
+      return hook;
     } else if ("jar".equals(url.getProtocol())) {
-      hooksh = File.createTempFile("hook_", ".sh");
       InputStream in = url.openStream();
       try {
-        FileOutputStream out = new FileOutputStream(hooksh);
+        hook = File.createTempFile("hook_", ".sh");
+        cleanup.add(hook);
+        FileOutputStream out = new FileOutputStream(hook);
         try {
           ByteStreams.copy(in, out);
         } finally {
@@ -114,21 +130,13 @@
       } finally {
         in.close();
       }
-      hook = hooksh;
+      hook.setExecutable(true);
+      hooks.put(name, hook);
+      return hook;
     } else {
       fail("Cannot invoke " + url);
-      hook = null;
+      return null;
     }
-
-    // The hook was copied out of our source control system into the
-    // target area by Java tools. Its not executable in the source
-    // are, nor did the copying Java program make it executable in the
-    // destination area. So we must force it to be executable.
-    //
-    final long time = hook.lastModified();
-    hook.setExecutable(true);
-    hook.setLastModified(time);
-    return hook;
   }
 
   private ClassLoader cl() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index 7294d4c..299a245 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -41,7 +41,7 @@
 import java.util.TreeSet;
 
 public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase {
-  private final static String THIS_SERVER = "localhost";
+  private static final String THIS_SERVER = "localhost";
   private GitRepositoryManager repoManager;
   private BlobBasedConfig bbc;
 
@@ -245,7 +245,7 @@
     assertEquals(expectedSubscriptions, returnedSubscriptions);
   }
 
-  private final static class SubmoduleSection {
+  private static final class SubmoduleSection {
     private final String url;
     private final String path;
     private final String branch;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index 303c497..a1e68d0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -14,40 +14,22 @@
 
 package com.google.gerrit.testutil;
 
-import static com.google.inject.Scopes.SINGLETON;
-
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.GerritPersonIdentProvider;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePath;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.schema.Current;
-import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.schema.SchemaVersion;
 import com.google.gwtorm.jdbc.Database;
 import com.google.gwtorm.jdbc.SimpleDataSource;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.Key;
+import com.google.inject.Inject;
 
 import junit.framework.TestCase;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
 
-import java.io.File;
 import java.io.IOException;
 import java.sql.Connection;
 import java.sql.SQLException;
@@ -64,6 +46,11 @@
  * the JVM running the unit tests doesn't run out of heap space.
  */
 public class InMemoryDatabase implements SchemaFactory<ReviewDb> {
+  public static InMemoryDatabase newDatabase() {
+    return Guice.createInjector(new InMemoryModule())
+        .getInstance(InMemoryDatabase.class);
+  }
+
   private static int dbCnt;
 
   private static synchronized DataSource newDataSource() throws SQLException {
@@ -81,15 +68,21 @@
     }
   }
 
+  private final SchemaVersion schemaVersion;
+  private final SchemaCreator schemaCreator;
+
   private Connection openHandle;
   private Database<ReviewDb> database;
   private boolean created;
-  private SchemaVersion schemaVersion;
-  private Injector injector;
 
-  public InMemoryDatabase() throws OrmException {
+  @Inject
+  InMemoryDatabase(SchemaVersion schemaVersion,
+      SchemaCreator schemaCreator) throws OrmException {
+    this.schemaVersion = schemaVersion;
+    this.schemaCreator = schemaCreator;
+
     try {
-      final DataSource dataSource = newDataSource();
+      DataSource dataSource = newDataSource();
 
       // Open one connection. This will peg the database into memory
       // until someone calls drop on us, allowing subsequent connections
@@ -101,54 +94,11 @@
       //
       database = new Database<ReviewDb>(dataSource, ReviewDb.class);
 
-      injector = Guice.createInjector(new AbstractModule() {
-        @Override
-        protected void configure() {
-          install(new SchemaVersion.Module());
-
-          bind(File.class) //
-              .annotatedWith(SitePath.class) //
-              .toInstance(new File("."));
-
-          Config cfg = new Config();
-          cfg.setString("gerrit", null, "basePath", "git");
-          cfg.setString("gerrit", null, "allProjects", "Test-Projects");
-          cfg.setString("user", null, "name", "Gerrit Code Review");
-          cfg.setString("user", null, "email", "gerrit@localhost");
-
-          bind(Config.class) //
-              .annotatedWith(GerritServerConfig.class) //
-              .toInstance(cfg);
-
-          bind(PersonIdent.class) //
-              .annotatedWith(GerritPersonIdent.class) //
-              .toProvider(GerritPersonIdentProvider.class);
-
-          bind(AllProjectsName.class) //
-              .toProvider(AllProjectsNameProvider.class);
-
-          bind(GitRepositoryManager.class) //
-              .to(InMemoryRepositoryManager.class).in(SINGLETON);
-
-          bind(String.class) //
-            .annotatedWith(AnonymousCowardName.class) //
-            .toProvider(AnonymousCowardNameProvider.class);
-
-          bind(DataSourceType.class) //
-            .to(InMemoryH2Type.class);
-        }
-      });
-      schemaVersion = injector.getInstance(
-          Key.get(SchemaVersion.class, Current.class));
     } catch (SQLException e) {
       throw new OrmException(e);
     }
   }
 
-  public <T> T getInstance(Class<T> clazz) {
-    return injector.getInstance(clazz);
-  }
-
   public Database<ReviewDb> getDatabase() {
     return database;
   }
@@ -165,7 +115,7 @@
       final ReviewDb c = open();
       try {
         try {
-          getInstance(SchemaCreator.class).create(c);
+          schemaCreator.create(c);
         } catch (IOException e) {
           throw new OrmException("Cannot create in-memory database", e);
         } catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
new file mode 100644
index 0000000..9714124
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -0,0 +1,219 @@
+// 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.testutil;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.DisabledChangeHooks;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.RemotePeer;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.CanonicalWebUrlModule;
+import com.google.gerrit.server.config.CanonicalWebUrlProvider;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.config.TrackingFootersProvider;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.PerThreadRequestScope;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexModule.IndexType;
+import com.google.gerrit.server.index.NoIndexModule;
+import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.schema.Current;
+import com.google.gerrit.server.schema.DataSourceType;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.SchemaVersion;
+import com.google.gerrit.server.ssh.NoSshKeyCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.servlet.RequestScoped;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+
+public class InMemoryModule extends FactoryModule {
+  public static Config newDefaultConfig() {
+    Config cfg = new Config();
+    cfg.setEnum("auth", null, "type", AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT);
+    cfg.setString("gerrit", null, "basePath", "git");
+    cfg.setString("gerrit", null, "allProjects", "Test-Projects");
+    cfg.setString("user", null, "name", "Gerrit Code Review");
+    cfg.setString("user", null, "email", "gerrit@localhost");
+    cfg.setBoolean("sendemail", null, "enable", false);
+    cfg.setString("cache", null, "directory", null);
+    return cfg;
+  }
+
+  private final Config cfg;
+
+  public InMemoryModule() {
+    this(newDefaultConfig());
+  }
+
+  public InMemoryModule(Config cfg) {
+    this.cfg = cfg;
+  }
+
+  public void inject(Object instance) {
+    Guice.createInjector(this).injectMembers(instance);
+  }
+
+  @Override
+  protected void configure() {
+    // For simplicity, don't create child injectors, just use this one to get a
+    // few required modules.
+    Injector cfgInjector = Guice.createInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(Config.class).annotatedWith(GerritServerConfig.class)
+            .toInstance(cfg);
+      }
+    });
+    install(cfgInjector.getInstance(GerritGlobalModule.class));
+
+    bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
+
+    install(new SchemaVersion.Module());
+
+    bind(File.class).annotatedWith(SitePath.class).toInstance(new File("."));
+    bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
+    try {
+      bind(SocketAddress.class).annotatedWith(RemotePeer.class)
+          .toInstance(new InetSocketAddress(InetAddress.getLocalHost(), 1234));
+    } catch (UnknownHostException e) {
+      throw newProvisionException(e);
+    }
+    bind(PersonIdent.class)
+        .annotatedWith(GerritPersonIdent.class)
+        .toProvider(GerritPersonIdentProvider.class);
+    bind(String.class)
+      .annotatedWith(AnonymousCowardName.class)
+      .toProvider(AnonymousCowardNameProvider.class);
+    bind(AllProjectsName.class)
+        .toProvider(AllProjectsNameProvider.class);
+    bind(GitRepositoryManager.class)
+        .to(InMemoryRepositoryManager.class);
+    bind(InMemoryRepositoryManager.class).in(SINGLETON);
+    bind(TrackingFooters.class).toProvider(TrackingFootersProvider.class)
+        .in(SINGLETON);
+
+    bind(DataSourceType.class)
+      .to(InMemoryH2Type.class);
+    bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {})
+        .to(InMemoryDatabase.class);
+
+    bind(ChangeHooks.class).to(DisabledChangeHooks.class);
+    install(NoSshKeyCache.module());
+    install(new CanonicalWebUrlModule() {
+      @Override
+      protected Class<? extends Provider<String>> provider() {
+        return CanonicalWebUrlProvider.class;
+      }
+    });
+    install(new DefaultCacheFactory.Module());
+    install(new SmtpEmailSender.Module());
+    install(new SignedTokenEmailTokenVerifier.Module());
+
+    IndexType indexType = null;
+    try {
+      indexType = cfg.getEnum("index", null, "type", IndexType.SQL);
+    } catch (IllegalArgumentException e) {
+      // Custom index type, caller must provide their own module.
+    }
+    if (indexType != null) {
+      switch (indexType) {
+        case LUCENE:
+          install(luceneIndexModule());
+          break;
+        case SQL:
+          install(new NoIndexModule());
+          break;
+        default:
+          throw new ProvisionException(
+              "index type unsupported in tests: " + indexType);
+      }
+    }
+  }
+
+  @Provides
+  @Singleton
+  InMemoryDatabase getInMemoryDatabase(@Current SchemaVersion schemaVersion,
+      SchemaCreator schemaCreator) throws OrmException {
+    return new InMemoryDatabase(schemaVersion, schemaCreator);
+  }
+
+  private Module luceneIndexModule() {
+    try {
+      int version = cfg.getInt("index", "lucene", "testVersion", -1);
+      checkState(ChangeSchemas.ALL.containsKey(version),
+          "invalid index.lucene.testVersion %s", version);
+      Class<?> clazz =
+          Class.forName("com.google.gerrit.lucene.LuceneIndexModule");
+      Constructor<?> c =
+          clazz.getConstructor(Integer.class, int.class, String.class);
+      return (Module) c.newInstance(Integer.valueOf(version), 0, (String) null);
+    } catch (ClassNotFoundException e) {
+      throw newProvisionException(e);
+    } catch (SecurityException e) {
+      throw newProvisionException(e);
+    } catch (NoSuchMethodException e) {
+      throw newProvisionException(e);
+    } catch (IllegalArgumentException e) {
+      throw newProvisionException(e);
+    } catch (InstantiationException e) {
+      throw newProvisionException(e);
+    } catch (IllegalAccessException e) {
+      throw newProvisionException(e);
+    } catch (InvocationTargetException e) {
+      throw newProvisionException(e);
+    }
+  }
+
+  private static ProvisionException newProvisionException(Throwable cause) {
+    ProvisionException pe = new ProvisionException(cause.getMessage());
+    pe.initCause(cause);
+    return pe;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 8e2bce6..c6626f0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -25,7 +25,6 @@
 import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.lib.Repository;
 
 import java.util.Map;
 import java.util.SortedSet;
@@ -55,13 +54,13 @@
   private Map<String, Repo> repos = Maps.newHashMap();
 
   @Override
-  public Repository openRepository(Project.NameKey name)
+  public InMemoryRepository openRepository(Project.NameKey name)
       throws RepositoryNotFoundException {
     return get(name);
   }
 
   @Override
-  public Repository createRepository(Project.NameKey name)
+  public InMemoryRepository createRepository(Project.NameKey name)
       throws RepositoryCaseMismatchException, RepositoryNotFoundException {
     Repo repo;
     try {
diff --git a/gerrit-solr/BUCK b/gerrit-solr/BUCK
new file mode 100644
index 0000000..c072830
--- /dev/null
+++ b/gerrit-solr/BUCK
@@ -0,0 +1,19 @@
+java_library(
+  name = 'solr',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-antlr:query_exception',
+    '//gerrit-extension-api:api',
+    '//gerrit-lucene:query_builder',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+    '//lib/lucene:core',
+    '//lib/solr:solrj',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
new file mode 100644
index 0000000..ddb86c3
--- /dev/null
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.solr;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+class IndexVersionCheck implements LifecycleListener {
+  public static final Map<String, Integer> SCHEMA_VERSIONS = ImmutableMap.of(
+      SolrChangeIndex.CHANGES_OPEN, ChangeSchemas.getLatest().getVersion(),
+      SolrChangeIndex.CHANGES_CLOSED, ChangeSchemas.getLatest().getVersion());
+
+  public static File solrIndexConfig(SitePaths sitePaths) {
+    return new File(sitePaths.index_dir, "gerrit_index.config");
+  }
+
+  private final SitePaths sitePaths;
+
+  @Inject
+  IndexVersionCheck(SitePaths sitePaths) {
+    this.sitePaths = sitePaths;
+  }
+
+  @Override
+  public void start() {
+    // TODO Query schema version from a special meta-document
+    File file = solrIndexConfig(sitePaths);
+    try {
+      FileBasedConfig cfg = new FileBasedConfig(file, FS.detect());
+      cfg.load();
+      for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
+        int schemaVersion = cfg.getInt("index", e.getKey(), "schemaVersion", 0);
+        if (schemaVersion != e.getValue()) {
+          throw new ProvisionException(String.format(
+              "wrong index schema version for \"%s\": expected %d, found %d%s",
+              e.getKey(), e.getValue(), schemaVersion, upgrade()));
+        }
+      }
+    } catch (IOException e) {
+      throw new ProvisionException("unable to read " + file);
+    } catch (ConfigInvalidException e) {
+      throw new ProvisionException("invalid config file " + file);
+    }
+  }
+
+  @Override
+  public void stop() {
+    // Do nothing.
+  }
+
+  private final String upgrade() {
+    return "\nRun reindex to rebuild the index:\n"
+        + "$ java -jar gerrit.war reindex -d "
+        + sitePaths.site_path.getAbsolutePath();
+  }
+}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
new file mode 100644
index 0000000..4f616b9
--- /dev/null
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
@@ -0,0 +1,337 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.solr;
+
+import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
+import static com.google.gerrit.solr.IndexVersionCheck.SCHEMA_VERSIONS;
+import static com.google.gerrit.solr.IndexVersionCheck.solrIndexConfig;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lucene.QueryBuilder;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.Schema.Values;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import org.apache.lucene.search.Query;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrServer;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrServer;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrInputDocument;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Secondary index implementation using a remote Solr instance. */
+class SolrChangeIndex implements ChangeIndex, LifecycleListener {
+  public static final String CHANGES_OPEN = "changes_open";
+  public static final String CHANGES_CLOSED = "changes_closed";
+  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+
+  private final FillArgs fillArgs;
+  private final SitePaths sitePaths;
+  private final IndexCollection indexes;
+  private final CloudSolrServer openIndex;
+  private final CloudSolrServer closedIndex;
+  private final Schema<ChangeData> schema;
+
+  SolrChangeIndex(
+      @GerritServerConfig Config cfg,
+      FillArgs fillArgs,
+      SitePaths sitePaths,
+      IndexCollection indexes,
+      Schema<ChangeData> schema,
+      String base) throws IOException {
+    this.fillArgs = fillArgs;
+    this.sitePaths = sitePaths;
+    this.indexes = indexes;
+    this.schema = schema;
+
+    String url = cfg.getString("index", "solr", "url");
+    if (Strings.isNullOrEmpty(url)) {
+      throw new IllegalStateException("index.solr.url must be supplied");
+    }
+
+    base = Strings.nullToEmpty(base);
+    openIndex = new CloudSolrServer(url);
+    openIndex.setDefaultCollection(base + CHANGES_OPEN);
+
+    closedIndex = new CloudSolrServer(url);
+    closedIndex.setDefaultCollection(base + CHANGES_CLOSED);
+  }
+
+  @Override
+  public void start() {
+    indexes.setSearchIndex(this);
+    indexes.addWriteIndex(this);
+  }
+
+  @Override
+  public void stop() {
+    openIndex.shutdown();
+    closedIndex.shutdown();
+  }
+
+  @Override
+  public Schema<ChangeData> getSchema() {
+    return schema;
+  }
+
+  @Override
+  public void close() {
+    stop();
+  }
+
+  @Override
+  public void insert(ChangeData cd) throws IOException {
+    String id = cd.getId().toString();
+    SolrInputDocument doc = toDocument(cd);
+    try {
+      if (cd.getChange().getStatus().isOpen()) {
+        closedIndex.deleteById(id);
+        openIndex.add(doc);
+      } else {
+        openIndex.deleteById(id);
+        closedIndex.add(doc);
+      }
+    } catch (SolrServerException e) {
+      throw new IOException(e);
+    }
+    commit(openIndex);
+    commit(closedIndex);
+  }
+
+  @Override
+  public void replace(ChangeData cd) throws IOException {
+    String id = cd.getId().toString();
+    SolrInputDocument doc = toDocument(cd);
+    try {
+      if (cd.getChange().getStatus().isOpen()) {
+        closedIndex.deleteById(id);
+        openIndex.add(doc);
+      } else {
+        openIndex.deleteById(id);
+        closedIndex.add(doc);
+      }
+    } catch (SolrServerException e) {
+      throw new IOException(e);
+    }
+    commit(openIndex);
+    commit(closedIndex);
+  }
+
+  @Override
+  public void delete(ChangeData cd) throws IOException {
+    String id = cd.getId().toString();
+    try {
+      if (cd.getChange().getStatus().isOpen()) {
+        openIndex.deleteById(id);
+        commit(openIndex);
+      } else {
+        closedIndex.deleteById(id);
+        commit(closedIndex);
+      }
+    } catch (SolrServerException e) {
+      throw new IOException(e);
+    }
+  }
+
+  @Override
+  public void deleteAll() throws IOException {
+    try {
+      openIndex.deleteByQuery("*:*");
+      closedIndex.deleteByQuery("*:*");
+    } catch (SolrServerException e) {
+      throw new IOException(e);
+    }
+    commit(openIndex);
+    commit(closedIndex);
+  }
+
+  @Override
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+      throws QueryParseException {
+    Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
+    List<SolrServer> indexes = Lists.newArrayListWithCapacity(2);
+    if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+      indexes.add(openIndex);
+    }
+    if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+      indexes.add(closedIndex);
+    }
+    return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
+        ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
+  }
+
+  private void commit(SolrServer server) throws IOException {
+    try {
+      server.commit();
+    } catch (SolrServerException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private class QuerySource implements ChangeDataSource {
+    private final List<SolrServer> indexes;
+    private final SolrQuery query;
+
+    public QuerySource(List<SolrServer> indexes, Query q, int limit,
+        boolean reverse) {
+      this.indexes = indexes;
+
+      query = new SolrQuery(q.toString());
+      query.setParam("shards.tolerant", true);
+      query.setParam("rows", Integer.toString(limit));
+      query.setFields(ID_FIELD);
+      query.setSort(
+          ChangeField.SORTKEY.getName(),
+          !reverse ? SolrQuery.ORDER.desc : SolrQuery.ORDER.asc);
+    }
+
+    @Override
+    public int getCardinality() {
+      return 10; // TODO: estimate from solr?
+    }
+
+    @Override
+    public boolean hasChange() {
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return query.getQuery();
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      try {
+        // TODO Sort documents during merge to select only top N.
+        SolrDocumentList docs = new SolrDocumentList();
+        for (SolrServer index : indexes) {
+          docs.addAll(index.query(query).getResults());
+        }
+
+        List<ChangeData> result = Lists.newArrayListWithCapacity(docs.size());
+        for (SolrDocument doc : docs) {
+          Integer v = (Integer) doc.getFieldValue(ID_FIELD);
+          result.add(new ChangeData(new Change.Id(v.intValue())));
+        }
+
+        final List<ChangeData> r = Collections.unmodifiableList(result);
+        return new ResultSet<ChangeData>() {
+          @Override
+          public Iterator<ChangeData> iterator() {
+            return r.iterator();
+          }
+
+          @Override
+          public List<ChangeData> toList() {
+            return r;
+          }
+
+          @Override
+          public void close() {
+            // Do nothing.
+          }
+        };
+      } catch (SolrServerException e) {
+        throw new OrmException(e);
+      }
+    }
+  }
+
+  private SolrInputDocument toDocument(ChangeData cd) throws IOException {
+    try {
+      SolrInputDocument result = new SolrInputDocument();
+      for (Values<ChangeData> values : schema.buildFields(cd, fillArgs)) {
+        add(result, values);
+      }
+      return result;
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private void add(SolrInputDocument doc, Values<ChangeData> values)
+      throws OrmException {
+    String name = values.getField().getName();
+    FieldType<?> type = values.getField().getType();
+
+    if (type == FieldType.INTEGER) {
+      for (Object value : values.getValues()) {
+        doc.addField(name, (Integer) value);
+      }
+    } else if (type == FieldType.LONG) {
+      for (Object value : values.getValues()) {
+        doc.addField(name, (Long) value);
+      }
+    } else if (type == FieldType.TIMESTAMP) {
+      for (Object v : values.getValues()) {
+        doc.addField(name, QueryBuilder.toIndexTime((Timestamp) v));
+      }
+    } else if (type == FieldType.EXACT
+        || type == FieldType.PREFIX
+        || type == FieldType.FULL_TEXT) {
+      for (Object value : values.getValues()) {
+        doc.addField(name, (String) value);
+      }
+    } else {
+      throw QueryBuilder.badFieldType(type);
+    }
+  }
+
+  @Override
+  public void markReady(boolean ready) throws IOException {
+    // TODO Move the schema version information to a special meta-document
+    FileBasedConfig cfg = new FileBasedConfig(
+        solrIndexConfig(sitePaths),
+        FS.detect());
+    for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
+      cfg.setInt("index", e.getKey(), "schemaVersion",
+          ready ? e.getValue() : -1);
+    }
+    cfg.save();
+  }
+}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
new file mode 100644
index 0000000..e73c6c1
--- /dev/null
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.solr;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+public class SolrIndexModule extends LifecycleModule {
+  private final boolean checkVersion;
+  private final int threads;
+  private final String base;
+
+  public SolrIndexModule() {
+    this(true, 0, null);
+  }
+
+  public SolrIndexModule(boolean checkVersion, int threads, String base) {
+    this.checkVersion = checkVersion;
+    this.threads = threads;
+    this.base = base;
+  }
+
+  @Override
+  protected void configure() {
+    install(new IndexModule(threads));
+    bind(ChangeIndex.class).to(SolrChangeIndex.class);
+    listener().to(SolrChangeIndex.class);
+    if (checkVersion) {
+      listener().to(IndexVersionCheck.class);
+    }
+  }
+
+  @Provides
+  @Singleton
+  public SolrChangeIndex getChangeIndex(@GerritServerConfig Config cfg,
+      SitePaths sitePaths,
+      IndexCollection indexes,
+      FillArgs fillArgs) throws IOException {
+    return new SolrChangeIndex(cfg, fillArgs, sitePaths, indexes,
+        ChangeSchemas.getLatest(), base);
+  }
+}
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
new file mode 100644
index 0000000..93a3ef7
--- /dev/null
+++ b/gerrit-sshd/BUCK
@@ -0,0 +1,39 @@
+SRCS = glob(['src/main/java/**/*.java'])
+
+java_library2(
+  name = 'sshd',
+  srcs = SRCS,
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-common:server',
+    '//gerrit-patch-jgit:server',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-util-cli:cli',
+    '//lib:args4j',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:jsch',
+    '//lib/commons:codec',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',  # SSH should not depend on servlet
+    '//lib/log:api',
+    '//lib/log:log4j',
+    '//lib/mina:core',
+    '//lib/mina:sshd',
+    '//lib/jgit:jgit',
+  ],
+  compile_deps = [
+    '//lib/bouncycastle:bcprov',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'sshd-src',
+  srcs = SRCS,
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
deleted file mode 100644
index ab9bd43..0000000
--- a/gerrit-sshd/pom.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-sshd</artifactId>
-  <name>Gerrit Code Review - SSHd</name>
-
-  <description>
-    Java SSH daemon with Gerrit commands and Git support
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jgit</groupId>
-      <artifactId>org.eclipse.jgit.junit</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.mina</groupId>
-      <artifactId>mina-core</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.sshd</groupId>
-      <artifactId>sshd-core</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-api</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-util-cli</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-server</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-cache-h2</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-  </dependencies>
-</project>
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 98b740c..4ac8f64 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
@@ -15,6 +15,8 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
@@ -25,6 +27,7 @@
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.EndOfOptionsHandler;
@@ -90,6 +93,11 @@
   @Inject
   private Provider<SshScope.Context> contextProvider;
 
+  /** Commands declared by a plugin can be scoped by the plugin name. */
+  @Inject(optional = true)
+  @PluginName
+  private String pluginName;
+
   /** The task, as scheduled on a worker thread. */
   private final AtomicReference<Future<?>> task;
 
@@ -119,7 +127,12 @@
     this.exit = callback;
   }
 
-  String getName() {
+  @Nullable
+  protected String getPluginName() {
+    return pluginName;
+  }
+
+  protected String getName() {
     return commandName;
   }
 
@@ -127,7 +140,7 @@
     this.commandName = prefix;
   }
 
-  String[] getArguments() {
+  public String[] getArguments() {
     return argv;
   }
 
@@ -326,7 +339,7 @@
     } else {
       final StringBuilder m = new StringBuilder();
       m.append("Internal server error");
-      if (userProvider.get() instanceof IdentifiedUser) {
+      if (userProvider.get().isIdentifiedUser()) {
         final IdentifiedUser u = (IdentifiedUser) userProvider.get();
         m.append(" (user ");
         m.append(u.getAccount().getUserName());
@@ -390,7 +403,7 @@
 
       StringBuilder m = new StringBuilder();
       m.append(context.getCommandLine());
-      if (userProvider.get() instanceof IdentifiedUser) {
+      if (userProvider.get().isIdentifiedUser()) {
         IdentifiedUser u = (IdentifiedUser) userProvider.get();
         m.append(" (" + u.getAccount().getUserName() + ")");
       }
@@ -417,7 +430,7 @@
         int rc = 0;
         final Context old = sshScope.set(context);
         try {
-          context.started = System.currentTimeMillis();
+          context.started = TimeUtil.nowMs();
           thisThread.setName("SSH " + taskName);
 
           if (thunk instanceof ProjectCommandRunnable) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 910f5f3..a2e6542 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -16,6 +16,7 @@
 
 import com.google.common.util.concurrent.Atomics;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
@@ -23,6 +24,7 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.CommandFactory;
@@ -39,7 +41,7 @@
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
@@ -49,7 +51,9 @@
 /**
  * Creates a CommandFactory using commands registered by {@link CommandModule}.
  */
-class CommandFactoryProvider implements Provider<CommandFactory> {
+@Singleton
+class CommandFactoryProvider implements Provider<CommandFactory>,
+    LifecycleListener {
   private static final Logger logger = LoggerFactory
       .getLogger(CommandFactoryProvider.class);
 
@@ -57,7 +61,7 @@
   private final SshLog log;
   private final SshScope sshScope;
   private final ScheduledExecutorService startExecutor;
-  private final Executor destroyExecutor;
+  private final ExecutorService destroyExecutor;
   private final SchemaFactory<ReviewDb> schemaFactory;
 
   @Inject
@@ -80,6 +84,15 @@
   }
 
   @Override
+  public void start() {
+  }
+
+  @Override
+  public void stop() {
+    destroyExecutor.shutdownNow();
+  }
+
+  @Override
   public CommandFactory get() {
     return new CommandFactory() {
       public Command createCommand(final String requestCommand) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
index cfcee6a..42c0fd7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
@@ -27,5 +27,9 @@
 @Retention(RUNTIME)
 public @interface CommandMetaData {
   String name();
+  String description() default "";
+
+  /** @deprecated use description intead. */
+  @Deprecated
   String descr() default "";
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
index e7e8a44..c64f9d8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
 import com.google.inject.AbstractModule;
 import com.google.inject.binder.LinkedBindingBuilder;
 
@@ -74,7 +76,7 @@
     if (meta == null) {
       throw new IllegalStateException("no CommandMetaData annotation found");
     }
-    bind(Commands.key(parent, meta.name(), meta.descr())).to(clazz);
+    bind(Commands.key(parent, meta.name(), description(meta))).to(clazz);
   }
 
   /**
@@ -93,7 +95,14 @@
     if (meta == null) {
       throw new IllegalStateException("no CommandMetaData annotation found");
     }
-    bind(Commands.key(parent, name, meta.descr())).to(clazz);
+    bind(Commands.key(parent, name, description(meta))).to(clazz);
+  }
+
+  @SuppressWarnings("deprecation")
+  private static String description(CommandMetaData meta) {
+    return Objects.firstNonNull(
+        Strings.emptyToNull(meta.description()),
+        meta.descr());
   }
 
   /**
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 929a895..1a5e62cc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -16,7 +16,6 @@
 
 import com.google.inject.Key;
 
-import org.apache.commons.lang.StringUtils;
 import org.apache.sshd.server.Command;
 
 import java.lang.annotation.Annotation;
@@ -124,7 +123,7 @@
     NestedCommandNameImpl(final CommandName parent, final String name) {
       this.parent = parent;
       this.name = name;
-      this.descr = StringUtils.EMPTY;
+      this.descr = "";
     }
 
     NestedCommandNameImpl(final CommandName parent, final String name,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 455b732..fa5ab53 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -17,9 +17,9 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Atomics;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.CapabilityUtils;
 import com.google.gerrit.server.args4j.SubcommandHandler;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -113,17 +113,18 @@
     }
   }
 
-  private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
-    RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
-    if (rc != null) {
-      CurrentUser user = currentUser.get();
-      CapabilityControl ctl = user.getCapabilities();
-      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
-        String msg = String.format(
-            "fatal: %s does not have \"%s\" capability.",
-            user.getUserName(), rc.value());
-        throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-      }
+  private void checkRequiresCapability(Command cmd)
+      throws UnloggedFailure {
+    String pluginName = null;
+    if (cmd instanceof BaseCommand) {
+      pluginName = ((BaseCommand) cmd).getPluginName();
+    }
+    try {
+      CapabilityUtils.checkRequiresCapability(currentUser,
+          pluginName, cmd.getClass());
+    } catch (AuthException e) {
+      throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN,
+          e.getMessage());
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 013f070..334f155 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -26,7 +26,7 @@
   private CommandName command;
 
   @Inject
-  void setPluginName(@PluginName String name, final String descr) {
+  void setPluginName(@PluginName String name) {
     this.command = Commands.named(name);
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshHostKeyModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshHostKeyModule.java
new file mode 100644
index 0000000..fdf34a2
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshHostKeyModule.java
@@ -0,0 +1,28 @@
+// 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.sshd;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.inject.AbstractModule;
+
+import org.apache.sshd.common.KeyPairProvider;
+
+public class SshHostKeyModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
+  }
+}
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 336490c..a78f9f5 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
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -71,24 +72,28 @@
     this.context = context;
     this.auditService = auditService;
 
-    final DailyRollingFileAppender dst = new DailyRollingFileAppender();
-    dst.setName(LOG_NAME);
-    dst.setLayout(new MyLayout());
-    dst.setEncoding("UTF-8");
-    dst.setFile(new File(resolve(site.logs_dir), LOG_NAME).getPath());
-    dst.setImmediateFlush(true);
-    dst.setAppend(true);
-    dst.setThreshold(Level.INFO);
-    dst.setErrorHandler(new DieErrorHandler());
-    dst.activateOptions();
-    dst.setErrorHandler(new LogLogHandler());
+    if (config.getBoolean("sshd", "requestLog", true)) {
+      final DailyRollingFileAppender dst = new DailyRollingFileAppender();
+      dst.setName(LOG_NAME);
+      dst.setLayout(new MyLayout());
+      dst.setEncoding("UTF-8");
+      dst.setFile(new File(resolve(site.logs_dir), LOG_NAME).getPath());
+      dst.setImmediateFlush(true);
+      dst.setAppend(true);
+      dst.setThreshold(Level.INFO);
+      dst.setErrorHandler(new DieErrorHandler());
+      dst.activateOptions();
+      dst.setErrorHandler(new LogLogHandler());
 
-    async = new AsyncAppender();
-    async.setBlocking(true);
-    async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
-    async.setLocationInfo(false);
-    async.addAppender(dst);
-    async.activateOptions();
+      async = new AsyncAppender();
+      async.setBlocking(true);
+      async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
+      async.setLocationInfo(false);
+      async.addAppender(dst);
+      async.activateOptions();
+    } else {
+      async = null;
+    }
   }
 
   @Override
@@ -97,11 +102,17 @@
 
   @Override
   public void stop() {
-    async.close();
+    if (async != null) {
+      async.close();
+    }
   }
 
   void onLogin() {
-    async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString()));
+    LoggingEvent entry =
+        log("LOGIN FROM " + session.get().getRemoteAddressAsString());
+    if (async != null) {
+      async.append(entry);
+    }
     audit(context.get(), "0", "LOGIN");
   }
 
@@ -109,7 +120,7 @@
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
         log, // logger
-        System.currentTimeMillis(), // when
+        TimeUtil.nowMs(), // when
         Level.INFO, // level
         "AUTH FAILURE FROM " + sd.getRemoteAddressAsString(), // message text
         "SSHD", // thread name
@@ -126,14 +137,15 @@
     if (error != null) {
       event.setProperty(P_STATUS, error);
     }
-
-    async.append(event);
+    if (async != null) {
+      async.append(event);
+    }
     audit(null, "FAIL", "AUTH");
   }
 
   void onExecute(DispatchCommand dcmd, int exitValue) {
     final Context ctx = context.get();
-    ctx.finished = System.currentTimeMillis();
+    ctx.finished = TimeUtil.nowMs();
 
     String cmd = extractWhat(dcmd);
 
@@ -161,7 +173,9 @@
     }
     event.setProperty(P_STATUS, status);
 
-    async.append(event);
+    if (async != null) {
+      async.append(event);
+    }
     audit(context.get(), status, dcmd);
   }
 
@@ -208,7 +222,10 @@
   }
 
   void onLogout() {
-    async.append(log("LOGOUT"));
+    LoggingEvent entry = log("LOGOUT");
+    if (async != null) {
+      async.append(entry);
+    }
     audit(context.get(), "0", "LOGOUT");
   }
 
@@ -219,7 +236,7 @@
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
         log, // logger
-        System.currentTimeMillis(), // when
+        TimeUtil.nowMs(), // when
         Level.INFO, // level
         msg, // message text
         "SSHD", // thread name
@@ -233,7 +250,7 @@
 
     String userName = "-", accountId = "-";
 
-    if (user instanceof IdentifiedUser) {
+    if (user != null && user.isIdentifiedUser()) {
       IdentifiedUser u = (IdentifiedUser) user;
       userName = u.getAccount().getUserName();
       accountId = "a/" + u.getAccountId().toString();
@@ -473,7 +490,7 @@
   }
 
   private long extractCreated(final Context ctx) {
-    return (ctx != null) ? ctx.created : System.currentTimeMillis();
+    return (ctx != null) ? ctx.created : TimeUtil.nowMs();
   }
 
   private CurrentUser extractCurrentUser(final Context ctx) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 2e42c23..39b7f16 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.sshd;
 
-import static com.google.inject.Scopes.SINGLETON;
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
+import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.collect.Maps;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.FactoryModule;
@@ -39,7 +38,6 @@
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.RequestScoped;
 
-import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.server.CommandFactory;
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
@@ -68,7 +66,6 @@
 
     configureRequestScope();
     install(new AsyncReceiveCommits.Module());
-    install(new CmdLineParserModule());
     configureAliases();
 
     bind(SshLog.class);
@@ -87,7 +84,6 @@
 
     bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
-    bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
 
     install(new DefaultCommandModule());
 
@@ -107,6 +103,7 @@
         listener().toInstance(registerInParentInjectors());
         listener().to(SshLog.class);
         listener().to(SshDaemon.class);
+        listener().to(CommandFactoryProvider.class);
       }
     });
   }
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 0ae40a5..440f236 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
@@ -81,7 +82,7 @@
     @Override
     public CurrentUser getCurrentUser() {
       final CurrentUser user = session.getCurrentUser();
-      if (user instanceof IdentifiedUser) {
+      if (user != null && user.isIdentifiedUser()) {
         IdentifiedUser identifiedUser = userFactory.create(((IdentifiedUser) user).getAccountId());
         identifiedUser.setAccessPath(user.getAccessPath());
         return identifiedUser;
@@ -165,7 +166,7 @@
   }
 
   Context newContext(SchemaFactory<ReviewDb> sf, SshSession s, String cmd) {
-    return new Context(sf, s, cmd, System.currentTimeMillis());
+    return new Context(sf, s, cmd, TimeUtil.nowMs());
   }
 
   private Context newContinuingContext(Context ctx) {
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 2cc16b5..04bf603 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
@@ -76,6 +76,7 @@
   void authenticationSuccess(String user, CurrentUser id) {
     username = user;
     identity = id;
+    identity.setAccessPath(AccessPath.SSH_COMMAND);
     authError = null;
   }
 
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 74e6d1b..a2f2c1d 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
@@ -79,7 +79,7 @@
       final StringBuilder strBuf = new StringBuilder();
       final BufferedReader br = new BufferedReader(new StringReader(keyStr));
       String line = br.readLine(); // BEGIN SSH2 line...
-      if (!line.equals("---- BEGIN SSH2 PUBLIC KEY ----")) {
+      if (line == null || !line.equals("---- BEGIN SSH2 PUBLIC KEY ----")) {
         return keyStr;
       }
 
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 ccf23e1..298a099 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -45,6 +46,7 @@
   private final SshScope sshScope;
   private final DispatchCommandProvider dispatcher;
 
+  private boolean enableRunAs;
   private Provider<CurrentUser> caller;
   private Provider<SshSession> session;
   private IdentifiedUser.GenericFactory userFactory;
@@ -66,36 +68,34 @@
       @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
       final Provider<CurrentUser> caller, final Provider<SshSession> session,
       final IdentifiedUser.GenericFactory userFactory,
-      final SshScope.Context callingContext) {
+      final SshScope.Context callingContext,
+      AuthConfig config) {
     this.sshScope = sshScope;
     this.dispatcher = dispatcher;
     this.caller = caller;
     this.session = session;
     this.userFactory = userFactory;
     this.callingContext = callingContext;
+    this.enableRunAs = config.isRunAsEnabled();
     atomicCmd = Atomics.newReference();
   }
 
   @Override
   public void start(Environment env) throws IOException {
     try {
-      if (caller.get() instanceof PeerDaemonUser) {
-        parseCommandLine();
+      checkCanRunAs();
+      parseCommandLine();
 
-        final Context ctx = callingContext.subContext(newSession(), join(args));
-        final Context old = sshScope.set(ctx);
-        try {
-          final BaseCommand cmd = dispatcher.get();
-          cmd.setArguments(args.toArray(new String[args.size()]));
-          provideStateTo(cmd);
-          atomicCmd.set(cmd);
-          cmd.start(env);
-        } finally {
-          sshScope.set(old);
-        }
-
-      } else {
-        throw new UnloggedFailure(1, "fatal: Not a peer daemon");
+      final Context ctx = callingContext.subContext(newSession(), join(args));
+      final Context old = sshScope.set(ctx);
+      try {
+        final BaseCommand cmd = dispatcher.get();
+        cmd.setArguments(args.toArray(new String[args.size()]));
+        provideStateTo(cmd);
+        atomicCmd.set(cmd);
+        cmd.start(env);
+      } finally {
+        sshScope.set(old);
       }
     } catch (UnloggedFailure e) {
       String msg = e.getMessage();
@@ -108,6 +108,17 @@
     }
   }
 
+  private void checkCanRunAs() throws UnloggedFailure {
+    if (caller.get() instanceof PeerDaemonUser) {
+      // OK.
+    } else if (!enableRunAs) {
+      throw new UnloggedFailure(1,
+          "fatal: suexec disabled by auth.enableRunAs = false");
+    } else if (!caller.get().getCapabilities().canRunAs()) {
+      throw new UnloggedFailure(1, "fatal: suexec not permitted");
+    }
+  }
+
   private SshSession newSession() {
     final SocketAddress peer;
     if (peerAddress == null) {
@@ -115,8 +126,12 @@
     } else {
       peer = peerAddress;
     }
+    CurrentUser self = caller.get();
+    if (self instanceof PeerDaemonUser) {
+      self = null;
+    }
     return new SshSession(session.get(), peer,
-        userFactory.create(peer, accountId));
+        userFactory.runAs(peer, accountId, self));
   }
 
   private static String join(List<String> args) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 15229dc..0ff3a01 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -28,7 +28,7 @@
 /** Opens a query processor. */
 @AdminHighPriorityCommand
 @RequiresCapability(GlobalCapability.ACCESS_DATABASE)
-@CommandMetaData(name = "gsql", descr = "Administrative interface to active database")
+@CommandMetaData(name = "gsql", description = "Administrative interface to active database")
 final class AdminQueryShell extends SshCommand {
   @Inject
   private QueryShell.Factory factory;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index faf2224..c68dc26 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -17,18 +17,23 @@
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -45,7 +50,7 @@
 import java.util.Set;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "set-project-parent", descr = "Change the project permissions are inherited from")
+@CommandMetaData(name = "set-project-parent", description = "Change the project permissions are inherited from")
 final class AdminSetParent extends SshCommand {
   private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
 
@@ -73,6 +78,9 @@
   @Inject
   private AllProjectsName allProjectsName;
 
+  @Inject
+  private Provider<ListChildProjects> listChildProjects;
+
   private Project.NameKey newParentKey = null;
 
   @Override
@@ -108,17 +116,16 @@
       }
     }
 
-    final List<Project> childProjects = new ArrayList<Project>();
+    final List<Project.NameKey> childProjects = Lists.newArrayList();
     for (final ProjectControl pc : children) {
-      childProjects.add(pc.getProject());
+      childProjects.add(pc.getProject().getNameKey());
     }
     if (oldParent != null) {
       childProjects.addAll(getChildrenForReparenting(oldParent));
     }
 
-    for (final Project project : childProjects) {
-      final String name = project.getName();
-      final Project.NameKey nameKey = project.getNameKey();
+    for (final Project.NameKey nameKey : childProjects) {
+      final String name = nameKey.get();
 
       if (allProjectsName.equals(nameKey)) {
         // Don't allow the wild card project to have a parent.
@@ -159,7 +166,7 @@
         err.append("error: " + msg + "\n");
       }
 
-      projectCache.evict(project);
+      projectCache.evict(nameKey);
     }
 
     if (err.length() > 0) {
@@ -175,8 +182,8 @@
    * reparented. The returned list of child projects does not contain projects
    * that were specified to be excluded from reparenting.
    */
-  private List<Project> getChildrenForReparenting(final ProjectControl parent) {
-    final List<Project> childProjects = new ArrayList<Project>();
+  private List<Project.NameKey> getChildrenForReparenting(final ProjectControl parent) {
+    final List<Project.NameKey> childProjects = Lists.newArrayList();
     final List<Project.NameKey> excluded =
       new ArrayList<Project.NameKey>(excludedChildren.size());
     for (final ProjectControl excludedChild : excludedChildren) {
@@ -187,11 +194,12 @@
     if (newParentKey != null) {
       automaticallyExcluded.addAll(getAllParents(newParentKey));
     }
-    for (final Project child : getChildren(parent.getProject().getNameKey())) {
-      final Project.NameKey childName = child.getNameKey();
+    for (final ProjectInfo child : listChildProjects.get().apply(
+        new ProjectResource(parent))) {
+      final Project.NameKey childName = new Project.NameKey(child.name);
       if (!excluded.contains(childName)) {
         if (!automaticallyExcluded.contains(childName)) {
-          childProjects.add(child);
+          childProjects.add(childName);
         } else {
           stdout.println("Automatically excluded '" + childName + "' " +
                          "from reparenting because it is in the parent " +
@@ -213,20 +221,4 @@
         }
       }));
   }
-
-  private List<Project> getChildren(final Project.NameKey parentName) {
-    final List<Project> childProjects = new ArrayList<Project>();
-    for (final Project.NameKey projectName : projectCache.all()) {
-      final ProjectState e = projectCache.get(projectName);
-      if (e == null) {
-        // If we can't get it from the cache, pretend it's not present.
-        continue;
-      }
-
-      if (parentName.equals(e.getProject().getParent(allProjectsName))) {
-        childProjects.add(e.getProject());
-      }
-    }
-    return childProjects;
-  }
 }
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 0268bc0..dc22a29 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
@@ -33,7 +33,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-@CommandMetaData(name = "ban-commit", descr = "Ban a commit from a project's repository")
+@CommandMetaData(name = "ban-commit", description = "Ban a commit from a project's repository")
 public class BanCommitCommand extends SshCommand {
   @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
   private String reason;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
index 500c84a..7f9e5db 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
@@ -28,10 +28,8 @@
 
   protected SortedSet<String> cacheNames() {
     SortedSet<String> names = Sets.newTreeSet();
-    for (String plugin : cacheMap.plugins()) {
-      for (String name : cacheMap.byPlugin(plugin).keySet()) {
-        names.add(cacheNameOf(plugin, name));
-      }
+    for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+      names.add(cacheNameOf(e.getPluginName(), e.getExportName()));
     }
     return names;
   }
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 c209249..aef1560 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,23 +14,16 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.account.CreateAccount;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -42,13 +35,11 @@
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 
 /** Create a new user account. **/
 @RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
-@CommandMetaData(name = "create-account", descr = "Create a new batch/role account")
+@CommandMetaData(name = "create-account", description = "Create a new batch/role account")
 final class CreateAccountCommand extends SshCommand {
   @Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
   private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
@@ -69,94 +60,29 @@
   private String username;
 
   @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
-  private ReviewDb db;
-
-  @Inject
-  private SshKeyCache sshKeyCache;
-
-  @Inject
-  private AccountCache accountCache;
-
-  @Inject
-  private AccountByEmailCache byEmailCache;
+  private CreateAccount.Factory createAccountFactory;
 
   @Override
-  protected void run() throws OrmException, IOException,
-      InvalidSshKeyException, UnloggedFailure {
-    if (!username.matches(Account.USER_NAME_PATTERN)) {
-      throw die("Username '" + username + "'"
-          + " must contain only letters, numbers, _, - or .");
-    }
-
-    final Account.Id id = new Account.Id(db.nextAccountId());
-    final AccountSshKey key = readSshKey(id);
-
-    AccountExternalId extUser =
-        new AccountExternalId(id, new AccountExternalId.Key(
-            AccountExternalId.SCHEME_USERNAME, username));
-
-    if (httpPassword != null) {
-      extUser.setPassword(httpPassword);
-    }
-
-    if (db.accountExternalIds().get(extUser.getKey()) != null) {
-      throw die("username '" + username + "' already exists");
-    }
-    if (email != null && db.accountExternalIds().get(getEmailKey()) != null) {
-      throw die("email '" + email + "' already exists");
-    }
-
+  protected void run() throws OrmException, IOException, UnloggedFailure {
+    CreateAccount.Input input = new CreateAccount.Input();
+    input.username = username;
+    input.email = email;
+    input.name = fullName;
+    input.sshKey = readSshKey();
+    input.httpPassword = httpPassword;
+    input.groups = Lists.transform(groups, new Function<AccountGroup.Id, String>() {
+      @Override
+      public String apply(AccountGroup.Id id) {
+        return id.toString();
+      }});
     try {
-      db.accountExternalIds().insert(Collections.singleton(extUser));
-    } catch (OrmDuplicateKeyException duplicateKey) {
-      throw die("username '" + username + "' already exists");
+      createAccountFactory.create(username).apply(TopLevelResource.INSTANCE, input);
+    } catch (RestApiException e) {
+      throw die(e.getMessage());
     }
-
-    if (email != null) {
-      AccountExternalId extMailto = new AccountExternalId(id, getEmailKey());
-      extMailto.setEmailAddress(email);
-      try {
-        db.accountExternalIds().insert(Collections.singleton(extMailto));
-      } catch (OrmDuplicateKeyException duplicateKey) {
-        try {
-          db.accountExternalIds().delete(Collections.singleton(extUser));
-        } catch (OrmException cleanupError) {
-        }
-        throw die("email '" + email + "' already exists");
-      }
-    }
-
-    Account a = new Account(id);
-    a.setFullName(fullName);
-    a.setPreferredEmail(email);
-    db.accounts().insert(Collections.singleton(a));
-
-    if (key != null) {
-      db.accountSshKeys().insert(Collections.singleton(key));
-    }
-
-    for (AccountGroup.Id groupId : new HashSet<AccountGroup.Id>(groups)) {
-      AccountGroupMember m =
-          new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
-      db.accountGroupMembersAudit().insert(Collections.singleton( //
-          new AccountGroupMemberAudit(m, currentUser.getAccountId())));
-      db.accountGroupMembers().insert(Collections.singleton(m));
-    }
-
-    sshKeyCache.evict(username);
-    accountCache.evictByUsername(username);
-    byEmailCache.evict(email);
   }
 
-  private AccountExternalId.Key getEmailKey() {
-    return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
-  }
-
-  private AccountSshKey readSshKey(final Account.Id id)
-      throws UnsupportedEncodingException, IOException, InvalidSshKeyException {
+  private String readSshKey() throws UnsupportedEncodingException, IOException {
     if (sshKey == null) {
       return null;
     }
@@ -169,6 +95,6 @@
         sshKey += line + "\n";
       }
     }
-    return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
+    return sshKey;
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 660460a..c652641 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -38,7 +38,7 @@
  * Optionally, puts an initial set of user in the newly created group.
  */
 @RequiresCapability(GlobalCapability.CREATE_GROUP)
-@CommandMetaData(name = "create-group", descr = "Create a new account group")
+@CommandMetaData(name = "create-group", description = "Create a new account group")
 final class CreateGroupCommand extends SshCommand {
   @Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
   private AccountGroup.Id ownerGroupId;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index bd624ae..24d7689 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -36,7 +36,7 @@
 
 /** Create a new project. **/
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
-@CommandMetaData(name = "create-project", descr = "Create a new project and associated Git repository")
+@CommandMetaData(name = "create-project", description = "Create a new project and associated Git repository")
 final class CreateProjectCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
   void setProjectNameFromOption(String name) {
@@ -106,6 +106,9 @@
   @Option(name = "--empty-commit", usage = "to create initial empty commit")
   private boolean createEmptyCommit;
 
+  @Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
+  private String maxObjectSizeLimit;
+
   private String projectName;
 
   @Argument(index = 0, metaVar = "NAME", usage = "name of project to be created")
@@ -143,6 +146,7 @@
         args.changeIdRequired = requireChangeID;
         args.branch = branch;
         args.createEmptyCommit = createEmptyCommit;
+        args.maxObjectSizeLimit = maxObjectSizeLimit;
 
         final PerformCreateProject createProject = factory.create(args);
         createProject.createProject();
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 35a4e14..521103b 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
@@ -39,6 +39,7 @@
     command(gerrit, BanCommitCommand.class);
     command(gerrit, FlushCaches.class);
     command(gerrit, ListProjectsCommand.class);
+    command(gerrit, ListMembersCommand.class);
     command(gerrit, ListGroupsCommand.class);
     command(gerrit, LsUserRefs.class);
     command(gerrit, Query.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 13abdf4..6c07dddb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -17,22 +17,21 @@
 import com.google.common.cache.Cache;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Option;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.SortedSet;
 
 /** Causes the caches to purge all entries and reload. */
 @RequiresCapability(GlobalCapability.FLUSH_CACHES)
-@CommandMetaData(name = "flush-caches", descr = "Flush some/all server caches from memory")
+@CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory")
 final class FlushCaches extends CacheCommand {
   private static final String WEB_SESSIONS = "web_sessions";
 
@@ -98,16 +97,13 @@
 
   private void doBulkFlush() {
     try {
-      for (String plugin : cacheMap.plugins()) {
-        for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
-            cacheMap.byPlugin(plugin).entrySet()) {
-          String n = cacheNameOf(plugin, entry.getKey());
-          if (flush(n)) {
-            try {
-              entry.getValue().get().invalidateAll();
-            } catch (Throwable err) {
-              stderr.println("error: cannot flush cache \"" + n + "\": " + err);
-            }
+      for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+        String n = cacheNameOf(e.getPluginName(), e.getExportName());
+        if (flush(n)) {
+          try {
+            e.getProvider().get().invalidateAll();
+          } catch (Throwable err) {
+            stderr.println("error: cannot flush cache \"" + n + "\": " + err);
           }
         }
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index c561153..346bea7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -37,7 +37,7 @@
 
 /** Runs the Git garbage collection. */
 @RequiresCapability(GlobalCapability.RUN_GC)
-@CommandMetaData(name = "gc", descr = "Run Git garbage collection")
+@CommandMetaData(name = "gc", description = "Run Git garbage collection")
 public class GarbageCollectionCommand extends BaseCommand {
 
   @Option(name = "--all", usage = "runs the Git garbage collection for all projects")
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 e5b4203..aadb1d9 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
@@ -37,7 +37,7 @@
 
 import java.io.PrintWriter;
 
-@CommandMetaData(name = "ls-groups", descr = "List groups visible to the caller")
+@CommandMetaData(name = "ls-groups", description = "List groups visible to the caller")
 public class ListGroupsCommand extends BaseCommand {
   @Inject
   private MyListGroups impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
new file mode 100644
index 0000000..b7dd380
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -0,0 +1,116 @@
+// 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.sshd.commands;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupDetailFactory.Factory;
+import com.google.gerrit.server.group.ListMembers;
+import com.google.gerrit.server.ioutil.ColumnFormatter;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gwtorm.server.OrmException;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * Implements a command that allows the user to see the members of a group.
+ */
+@CommandMetaData(name = "ls-members", description = "Lists the members of a given group")
+public class ListMembersCommand extends BaseCommand {
+  @Inject
+  ListMembersCommandImpl impl;
+
+  @Override
+  public void start(Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine(impl);
+        final PrintWriter stdout = toPrintWriter(out);
+        try {
+          impl.display(stdout);
+        } finally {
+          stdout.flush();
+        }
+      }
+    });
+  }
+
+  private static class ListMembersCommandImpl extends ListMembers {
+    @Argument(required = true, usage = "the name of the group", metaVar = "GROUPNAME")
+    private String name;
+
+    private final GroupCache groupCache;
+
+    @Inject
+    protected ListMembersCommandImpl(GroupCache groupCache,
+        Factory groupDetailFactory,
+        AccountInfo.Loader.Factory accountLoaderFactory,
+        AccountCache accountCache) {
+      super(groupCache, groupDetailFactory, accountLoaderFactory);
+      this.groupCache = groupCache;
+    }
+
+    void display(PrintWriter writer) throws UnloggedFailure, OrmException {
+      AccountGroup group = groupCache.get(new AccountGroup.NameKey(name));
+      String errorText = "Group not found or not visible\n";
+
+      if (group == null) {
+        writer.write(errorText);
+        writer.flush();
+        return;
+      }
+
+      try {
+        List<AccountInfo> members = apply(group.getGroupUUID());
+        ColumnFormatter formatter = new ColumnFormatter(writer, '\t');
+        formatter.addColumn("id");
+        formatter.addColumn("username");
+        formatter.addColumn("full name");
+        formatter.addColumn("email");
+        formatter.nextLine();
+        for (AccountInfo member : members) {
+          if (member == null) {
+            continue;
+          }
+
+          formatter.addColumn(member._id.toString());
+          formatter.addColumn(Objects.firstNonNull(member.username, "n/a"));
+          formatter.addColumn(Objects.firstNonNull(
+              Strings.emptyToNull(member.name), "n/a"));
+          formatter.addColumn(Objects.firstNonNull(member.email, "n/a"));
+          formatter.nextLine();
+        }
+
+        formatter.finish();
+      } catch (MethodNotAllowedException e) {
+        writer.write(errorText);
+        writer.flush();
+      }
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index ab70395..8bcae4b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -23,7 +23,7 @@
 
 import java.util.List;
 
-@CommandMetaData(name = "ls-projects", descr = "List projects visible to the caller")
+@CommandMetaData(name = "ls-projects", description = "List projects visible to the caller")
 final class ListProjectsCommand extends BaseCommand {
   @Inject
   private ListProjects impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 58abf95..b071e3d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Account;
@@ -40,7 +42,7 @@
 import java.util.Map;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "ls-user-refs", descr = "List refs visible to a specific user")
+@CommandMetaData(name = "ls-user-refs", description = "List refs visible to a specific user")
 public class LsUserRefs extends SshCommand {
   @Inject
   private AccountResolver accountResolver;
@@ -88,26 +90,31 @@
 
     IdentifiedUser user = userFactory.create(userAccount.getId());
     ProjectControl userProjectControl = projectControl.forUser(user);
-    Repository repo = null;
+    Repository repo;
     try {
       repo = repoManager.openRepository(userProjectControl.getProject()
               .getNameKey());
-
-      Map<String, Ref> refsMap =
-          new VisibleRefFilter(tagCache, changeCache, repo, userProjectControl,
-              db, true).filter(repo.getAllRefs(), false);
-
-      for (final String ref : refsMap.keySet()) {
-        if (!onlyRefsHeads || ref.startsWith(Branch.R_HEADS)) {
-          stdout.println(ref);
-        }
-      }
     } catch (RepositoryNotFoundException e) {
       throw new UnloggedFailure("fatal: '"
           + projectControl.getProject().getNameKey() + "': not a git archive");
     } catch (IOException e) {
       throw new UnloggedFailure("fatal: Error opening: '"
           + projectControl.getProject().getNameKey());
+    }
+
+    try {
+      Map<String, Ref> refsMap =
+          new VisibleRefFilter(tagCache, changeCache, repo, userProjectControl,
+              db, true).filter(repo.getRefDatabase().getRefs(ALL), false);
+
+      for (final String ref : refsMap.keySet()) {
+        if (!onlyRefsHeads || ref.startsWith(Branch.R_HEADS)) {
+          stdout.println(ref);
+        }
+      }
+    } catch (IOException e) {
+      throw new Failure(1, "fatal: Error reading refs: '"
+          + projectControl.getProject().getNameKey(), e);
     } finally {
       repo.close();
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 30f3c95..abe202e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -39,6 +39,7 @@
     // deprecated alias to review command
     alias(gerrit, "approve", ReviewCommand.class);
     command(gerrit, SetAccountCommand.class);
+    command(gerrit, SetMembersCommand.class);
     command(gerrit, SetProjectCommand.class);
 
     command(gerrit, "test-submit").toProvider(new DispatchCommandProvider(testSubmit));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
index 709e337..47c2d68 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
@@ -28,7 +28,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "enable", descr = "Enable plugins")
+@CommandMetaData(name = "enable", description = "Enable plugins")
 final class PluginEnableCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin(s) to enable")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index fc036fe..70d09ee 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -35,7 +35,7 @@
 import java.net.URL;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "install", descr = "Install/Add a plugin")
+@CommandMetaData(name = "install", description = "Install/Add a plugin")
 final class PluginInstallCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, usage = "install under name")
   private String name;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index ab6c978..7e44641 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -26,7 +26,7 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "ls", descr = "List the installed plugins")
+@CommandMetaData(name = "ls", description = "List the installed plugins")
 final class PluginLsCommand extends BaseCommand {
   @Inject
   private ListPlugins impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index 85ade03..3ed1011 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -28,7 +28,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "reload", descr = "Reload/Restart plugins")
+@CommandMetaData(name = "reload", description = "Reload/Restart plugins")
 final class PluginReloadCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
   private List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
index 96adb8fe..0ae11af 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -27,7 +27,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "remove", descr = "Disable plugins")
+@CommandMetaData(name = "remove", description = "Disable plugins")
 final class PluginRemoveCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index 2d17876..185bb67 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -24,7 +24,7 @@
 
 import java.util.List;
 
-@CommandMetaData(name = "query", descr = "Query the change database")
+@CommandMetaData(name = "query", description = "Query the change database")
 class Query extends SshCommand {
   @Inject
   private QueryProcessor processor;
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 1630d11..5649843 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
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.common.Version;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -49,7 +51,7 @@
   }
 
   public static enum OutputFormat {
-    PRETTY, JSON;
+    PRETTY, JSON, JSON_SINGLE;
   }
 
   private final BufferedReader in;
@@ -178,6 +180,7 @@
         } else {
           final String msg = "'\\" + line + "' not supported";
           switch (outputFormat) {
+            case JSON_SINGLE:
             case JSON: {
               final JsonObject err = new JsonObject();
               err.addProperty("type", "error");
@@ -228,9 +231,9 @@
         if (outputFormat == OutputFormat.PRETTY) {
           println("                     List of relations");
         }
-        showResultSet(rs, false, //
-            Identity.create(rs, "TABLE_SCHEM"), //
-            Identity.create(rs, "TABLE_NAME"), //
+        showResultSet(rs, false, 0,
+            Identity.create(rs, "TABLE_SCHEM"),
+            Identity.create(rs, "TABLE_NAME"),
             Identity.create(rs, "TABLE_TYPE"));
       } finally {
         rs.close();
@@ -267,8 +270,8 @@
         if (outputFormat == OutputFormat.PRETTY) {
           println("                     Table " + tableName);
         }
-        showResultSet(rs, true, //
-            Identity.create(rs, "COLUMN_NAME"), //
+        showResultSet(rs, true, 0,
+            Identity.create(rs, "COLUMN_NAME"),
             new Function("TYPE") {
               @Override
               String apply(final ResultSet rs) throws SQLException {
@@ -352,7 +355,7 @@
   }
 
   private void executeStatement(final String sql) {
-    final long start = System.currentTimeMillis();
+    final long start = TimeUtil.nowMs();
     final boolean hasResultSet;
     try {
       hasResultSet = statement.execute(sql);
@@ -365,32 +368,16 @@
       if (hasResultSet) {
         final ResultSet rs = statement.getResultSet();
         try {
-          final int rowCount = showResultSet(rs, false);
-          final long ms = System.currentTimeMillis() - start;
-          switch (outputFormat) {
-            case JSON: {
-              final JsonObject tail = new JsonObject();
-              tail.addProperty("type", "query-stats");
-              tail.addProperty("rowCount", rowCount);
-              tail.addProperty("runTimeMilliseconds", ms);
-              println(tail.toString());
-              break;
-            }
-
-            case PRETTY:
-            default:
-              println("(" + rowCount + (rowCount == 1 ? " row" : " rows") //
-                  + "; " + ms + " ms)");
-              break;
-          }
+          showResultSet(rs, false, start);
         } finally {
           rs.close();
         }
 
       } else {
         final int updateCount = statement.getUpdateCount();
-        final long ms = System.currentTimeMillis() - start;
+        final long ms = TimeUtil.nowMs() - start;
         switch (outputFormat) {
+          case JSON_SINGLE:
           case JSON: {
             final JsonObject tail = new JsonObject();
             tail.addProperty("type", "update-stats");
@@ -411,19 +398,47 @@
     }
   }
 
-  private int showResultSet(final ResultSet rs, boolean alreadyOnRow,
-      Function... show) throws SQLException {
+  /**
+   * Outputs a result set to stdout.
+   *
+   * @param rs ResultSet to show.
+   * @param alreadyOnRow true if rs is already on the first row. false
+   *     otherwise.
+   * @param start Timestamp in milliseconds when executing the statement
+   *     started. This timestamp is used to compute statistics about the
+   *     statement. If no statistics should be shown, set it to 0.
+   * @param show Functions to map columns
+   * @throws SQLException
+   */
+  private void showResultSet(final ResultSet rs, boolean alreadyOnRow,
+      long start, Function... show) throws SQLException {
     switch (outputFormat) {
+      case JSON_SINGLE:
       case JSON:
-        return showResultSetJson(rs, alreadyOnRow, show);
+        showResultSetJson(rs, alreadyOnRow,  start, show);
+        break;
       case PRETTY:
       default:
-        return showResultSetPretty(rs, alreadyOnRow, show);
+        showResultSetPretty(rs, alreadyOnRow, start, show);
+        break;
     }
   }
 
-  private int showResultSetJson(final ResultSet rs, boolean alreadyOnRow,
-      Function... show) throws SQLException {
+  /**
+   * Outputs a result set to stdout in Json format.
+   *
+   * @param rs ResultSet to show.
+   * @param alreadyOnRow true if rs is already on the first row. false
+   *     otherwise.
+   * @param start Timestamp in milliseconds when executing the statement
+   *     started. This timestamp is used to compute statistics about the
+   *     statement. If no statistics should be shown, set it to 0.
+   * @param show Functions to map columns
+   * @throws SQLException
+   */
+  private void showResultSetJson(final ResultSet rs, boolean alreadyOnRow,
+      long start, Function... show) throws SQLException {
+    JsonArray collector = new JsonArray();
     final ResultSetMetaData meta = rs.getMetaData();
     final Function[] columnMap;
     if (show != null && 0 < show.length) {
@@ -453,15 +468,68 @@
       }
       row.addProperty("type", "row");
       row.add("columns", cols);
-      println(row.toString());
+      switch (outputFormat) {
+        case JSON:
+          println(row.toString());
+          break;
+        case JSON_SINGLE:
+          collector.add(row);
+          break;
+        default:
+          final JsonObject obj = new JsonObject();
+          obj.addProperty("type", "error");
+          obj.addProperty("message", "Unsupported Json variant");
+          println(obj.toString());
+          return;
+      }
       alreadyOnRow = false;
       rowCnt++;
     }
-    return rowCnt;
+
+    JsonObject tail = null;
+    if (start != 0) {
+      tail = new JsonObject();
+      tail.addProperty("type", "query-stats");
+      tail.addProperty("rowCount", rowCnt);
+      final long ms = TimeUtil.nowMs() - start;
+      tail.addProperty("runTimeMilliseconds", ms);
+    }
+
+    switch (outputFormat) {
+      case JSON:
+        if (tail != null) {
+          println(tail.toString());
+        }
+        break;
+      case JSON_SINGLE:
+        if (tail != null) {
+          collector.add(tail);
+        }
+        println(collector.toString());
+        break;
+      default:
+        final JsonObject obj = new JsonObject();
+        obj.addProperty("type", "error");
+        obj.addProperty("message", "Unsupported Json variant");
+        println(obj.toString());
+        return;
+    }
   }
 
-  private int showResultSetPretty(final ResultSet rs, boolean alreadyOnRow,
-      Function... show) throws SQLException {
+  /**
+   * Outputs a result set to stdout in plain text format.
+   *
+   * @param rs ResultSet to show.
+   * @param alreadyOnRow true if rs is already on the first row. false
+   *     otherwise.
+   * @param start Timestamp in milliseconds when executing the statement
+   *     started. This timestamp is used to compute statistics about the
+   *     statement. If no statistics should be shown, set it to 0.
+   * @param show Functions to map columns
+   * @throws SQLException
+   */
+  private void showResultSetPretty(final ResultSet rs, boolean alreadyOnRow,
+      long start, Function... show) throws SQLException {
     final ResultSetMetaData meta = rs.getMetaData();
 
     final Function[] columnMap;
@@ -559,11 +627,18 @@
     if (dataTruncated) {
       warning("some column data was truncated");
     }
-    return rows.size();
+
+    if (start != 0) {
+      final int rowCount = rows.size();
+      final long ms = TimeUtil.nowMs() - start;
+      println("(" + rowCount + (rowCount == 1 ? " row" : " rows")
+          + "; " + ms + " ms)");
+    }
   }
 
   private void warning(final String msg) {
     switch (outputFormat) {
+      case JSON_SINGLE:
       case JSON: {
         final JsonObject obj = new JsonObject();
         obj.addProperty("type", "warning");
@@ -581,6 +656,7 @@
 
   private void error(final SQLException err) {
     switch (outputFormat) {
+      case JSON_SINGLE:
       case JSON: {
         final JsonObject obj = new JsonObject();
         obj.addProperty("type", "error");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index e0dd38f..31f9301 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -28,6 +28,7 @@
 import org.eclipse.jgit.errors.TooLargeObjectInPackException;
 import org.eclipse.jgit.errors.UnpackException;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.kohsuke.args4j.Option;
@@ -42,7 +43,7 @@
 import java.util.Set;
 
 /** Receives change upload over SSH using the Git receive-pack protocol. */
-@CommandMetaData(name = "receive-pack", descr = "Standard Git server side command for client side git push")
+@CommandMetaData(name = "receive-pack", description = "Standard Git server side command for client side git push")
 final class Receive extends AbstractGitCommand {
   private static final Logger log = LoggerFactory.getLogger(Receive.class);
 
@@ -94,9 +95,9 @@
     final ReceivePack rp = receive.getReceivePack();
     rp.setRefLogIdent(currentUser.newRefLogIdent());
     rp.setTimeout(config.getTimeout());
-    rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
+    rp.setMaxObjectSizeLimit(config.getEffectiveMaxObjectSizeLimit(
+        projectControl.getProjectState()));
     try {
-      receive.advertiseHistory();
       rp.receive(in, out, err);
     } catch (UnpackException badStream) {
       // In case this was caused by the user pushing an object whose size
@@ -141,8 +142,10 @@
               + ref.getName() + "\n");
         }
 
+        Map<String, Ref> allRefs =
+            rp.getRepository().getRefDatabase().getRefs(RefDatabase.ALL);
         List<Ref> hidden = new ArrayList<Ref>();
-        for (Ref ref : rp.getRepository().getAllRefs().values()) {
+        for (Ref ref : allRefs.values()) {
           if (!adv.containsKey(ref.getName())) {
             hidden.add(ref);
           }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index f3c1bb3..38535a4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -25,7 +25,7 @@
 
 import org.kohsuke.args4j.Argument;
 
-@CommandMetaData(name = "rename-group", descr = "Rename an account group")
+@CommandMetaData(name = "rename-group", description = "Rename an account group")
 public class RenameGroupCommand extends SshCommand {
   @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
   private String groupName;
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 5769a22..e5eb567 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
@@ -31,11 +31,11 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.Abandon;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.DeleteDraftPatchSet;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
-import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
 import com.google.gerrit.server.changedetail.PublishDraft;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.project.ChangeControl;
@@ -63,7 +63,7 @@
 import java.util.Map;
 import java.util.Set;
 
-@CommandMetaData(name = "review", descr = "Verify, approve and/or submit one or more patch sets")
+@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets")
 public class ReviewCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(ReviewCommand.class);
@@ -105,10 +105,6 @@
   @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)")
   private boolean submitChange;
 
-  @Option(name = "--force-message", usage = "publish the message, "
-      + "even if the label score cannot be applied due to the change being closed")
-  private boolean forceMessage = false;
-
   @Option(name = "--publish", usage = "publish the specified draft patch set(s)")
   private boolean publishPatchSet;
 
@@ -135,7 +131,7 @@
   private ReviewDb db;
 
   @Inject
-  private DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
+  private DeleteDraftPatchSet deleteDraftPatchSetImpl;
 
   @Inject
   private ProjectControl.Factory projectControlFactory;
@@ -288,6 +284,16 @@
             new ChangeResource(ctl), patchSet),
           input);
       }
+
+      if (publishPatchSet) {
+        final ReviewResult result =
+            publishDraftFactory.create(patchSet.getId()).call();
+        handleReviewResultErrors(result);
+      } else if (deleteDraftPatchSet) {
+        deleteDraftPatchSetImpl.apply(new RevisionResource(
+            new ChangeResource(ctl), patchSet),
+            new DeleteDraftPatchSet.Input());
+      }
     } catch (InvalidChangeOperationException e) {
       throw error(e.getMessage());
     } catch (IllegalStateException e) {
@@ -299,16 +305,6 @@
     } catch (ResourceConflictException e) {
       throw error(e.getMessage());
     }
-
-    if (publishPatchSet) {
-      final ReviewResult result =
-          publishDraftFactory.create(patchSet.getId()).call();
-      handleReviewResultErrors(result);
-    } else if (deleteDraftPatchSet) {
-      final ReviewResult result =
-          deleteDraftPatchSetFactory.create(patchSet.getId()).call();
-      handleReviewResultErrors(result);
-    }
   }
 
   private void handleReviewResultErrors(final ReviewResult result) {
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 c938891..5736bb6 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,32 +14,40 @@
 
 package com.google.gerrit.sshd.commands;
 
-
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.RawInput;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AddSshKey;
+import com.google.gerrit.server.account.CreateEmail;
+import com.google.gerrit.server.account.DeleteActive;
+import com.google.gerrit.server.account.DeleteEmail;
+import com.google.gerrit.server.account.DeleteSshKey;
+import com.google.gerrit.server.account.GetEmails;
+import com.google.gerrit.server.account.GetEmails.EmailInfo;
+import com.google.gerrit.server.account.GetSshKeys;
+import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+import com.google.gerrit.server.account.PutActive;
+import com.google.gerrit.server.account.PutHttpPassword;
+import com.google.gerrit.server.account.PutName;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
@@ -47,7 +55,7 @@
 import java.util.List;
 
 /** Set a user's account settings. **/
-@CommandMetaData(name = "set-account", descr = "Change an account's settings")
+@CommandMetaData(name = "set-account", description = "Change an account's settings")
 final class SetAccountCommand extends BaseCommand {
 
   @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
@@ -81,19 +89,40 @@
   private IdentifiedUser currentUser;
 
   @Inject
-  private ReviewDb db;
+  private IdentifiedUser.GenericFactory genericUserFactory;
 
   @Inject
-  private AccountManager manager;
+  private CreateEmail.Factory createEmailFactory;
 
   @Inject
-  private SshKeyCache sshKeyCache;
+  private Provider<GetEmails> getEmailsProvider;
 
   @Inject
-  private AccountCache byIdCache;
+  private Provider<DeleteEmail> deleteEmailProvider;
 
   @Inject
-  private Realm realm;
+  private Provider<PutName> putNameProvider;
+
+  @Inject
+  private Provider<PutHttpPassword> putHttpPasswordProvider;
+
+  @Inject
+  private Provider<PutActive> putActiveProvider;
+
+  @Inject
+  private Provider<DeleteActive> deleteActiveProvider;
+
+  @Inject
+  private Provider<AddSshKey> addSshKeyProvider;
+
+  @Inject
+  private Provider<GetSshKeys> getSshKeysProvider;
+
+  @Inject
+  private Provider<DeleteSshKey> deleteSshKeyProvider;
+
+  private IdentifiedUser user;
+  private AccountResource rsrc;
 
   @Override
   public void start(final Environment env) {
@@ -131,151 +160,127 @@
   }
 
   private void setAccount() throws OrmException, IOException, UnloggedFailure {
-
-    final Account account = db.accounts().get(id);
-    boolean accountUpdated = false;
-    boolean sshKeysUpdated = false;
-
-    ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
-    for (AccountExternalId extId : ids) {
-      if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
-        account.setUserName(extId.getSchemeRest());
+    user = genericUserFactory.create(id);
+    rsrc = new AccountResource(user);
+    try {
+      for (String email : addEmails) {
+        addEmail(email);
       }
-    }
 
-    for (String email : addEmails) {
-      link(id, email);
-    }
-
-    for (String email : deleteEmails) {
-      deleteMail(id, email);
-    }
-
-    if (fullName != null) {
-      if (realm.allowsEdit(FieldName.FULL_NAME)) {
-        account.setFullName(fullName);
-        accountUpdated = true;
-      } else {
-        throw new UnloggedFailure(1, "The realm doesn't allow editing names");
+      for (String email : deleteEmails) {
+        deleteEmail(email);
       }
-    }
 
-    if (httpPassword != null) {
-      setHttpPassword(id, httpPassword);
-    }
+      if (fullName != null) {
+        PutName.Input in = new PutName.Input();
+        in.name = fullName;
+        putNameProvider.get().apply(rsrc, in);
+      }
 
-    if (active) {
-      accountUpdated = true;
-      account.setActive(true);
-    } else if (inactive) {
-      accountUpdated = true;
-      account.setActive(false);
-    }
+      if (httpPassword != null) {
+        PutHttpPassword.Input in = new PutHttpPassword.Input();
+        in.httpPassword = httpPassword;
+        putHttpPasswordProvider.get().apply(rsrc, in);
+      }
 
-    addSshKeys = readSshKey(addSshKeys);
-    if (!addSshKeys.isEmpty()) {
-      sshKeysUpdated = true;
-      addSshKeys(addSshKeys, account);
-    }
+      if (active) {
+        putActiveProvider.get().apply(rsrc, null);
+      } else if (inactive) {
+        try {
+          deleteActiveProvider.get().apply(rsrc, null);
+        } catch (ResourceNotFoundException e) {
+          // user is already inactive
+        }
+      }
 
-    deleteSshKeys = readSshKey(deleteSshKeys);
-    if (!deleteSshKeys.isEmpty()) {
-      sshKeysUpdated = true;
-      deleteSshKeys(deleteSshKeys, account);
-    }
+      addSshKeys = readSshKey(addSshKeys);
+      if (!addSshKeys.isEmpty()) {
+        addSshKeys(addSshKeys);
+      }
 
-    if (accountUpdated) {
-      db.accounts().update(Collections.singleton(account));
-      byIdCache.evict(id);
-    }
-
-    if (sshKeysUpdated) {
-      sshKeyCache.evict(account.getUserName());
+      deleteSshKeys = readSshKey(deleteSshKeys);
+      if (!deleteSshKeys.isEmpty()) {
+        deleteSshKeys(deleteSshKeys);
+      }
+    } catch (RestApiException e) {
+      throw die(e.getMessage());
     }
   }
 
-  private void addSshKeys(final List<String> keys, final Account account)
-      throws OrmException, UnloggedFailure {
-    List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
-    int seq = db.accountSshKeys().byAccount(account.getId()).toList().size();
-    for (String key : keys) {
-      try {
-        seq++;
-        AccountSshKey accountSshKey = sshKeyCache.create(
-            new AccountSshKey.Id(account.getId(), seq), key.trim());
-        accountKeys.add(accountSshKey);
-      } catch (InvalidSshKeyException e) {
-        throw new UnloggedFailure(1, "fatal: invalid ssh key");
-      }
+  private void addSshKeys(List<String> sshKeys) throws RestApiException,
+      UnloggedFailure, OrmException, IOException {
+    for (final String sshKey : sshKeys) {
+      AddSshKey.Input in = new AddSshKey.Input();
+      in.raw = new RawInput() {
+        @Override
+        public InputStream getInputStream() throws IOException {
+          return new ByteArrayInputStream(sshKey.getBytes("UTF-8"));
+        }
+
+        @Override
+        public String getContentType() {
+          return "plain/text";
+        }
+
+        @Override
+        public long getContentLength() {
+          return sshKey.length();
+        }
+      };
+      addSshKeyProvider.get().apply(rsrc, in);
     }
-    db.accountSshKeys().insert(accountKeys);
   }
 
-  private void deleteSshKeys(final List<String> keys, final Account account)
-      throws OrmException {
-    ResultSet<AccountSshKey> allKeys = db.accountSshKeys().byAccount(account.getId());
-    if (keys.contains("ALL")) {
-      db.accountSshKeys().delete(allKeys);
+  private void deleteSshKeys(List<String> sshKeys) throws RestApiException,
+      OrmException {
+    List<SshKeyInfo> infos = getSshKeysProvider.get().apply(rsrc);
+    if (sshKeys.contains("ALL")) {
+      for (SshKeyInfo i : infos) {
+        deleteSshKey(i);
+      }
     } else {
-      List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
-      for (String key : keys) {
-        for (AccountSshKey accountSshKey : allKeys) {
-          if (key.trim().equals(accountSshKey.getSshPublicKey())
-              || accountSshKey.getComment().trim().equals(key)) {
-            accountKeys.add(accountSshKey);
+      for (String sshKey : sshKeys) {
+        for (SshKeyInfo i : infos) {
+          if (sshKey.trim().equals(i.sshPublicKey)
+              || sshKey.trim().equals(i.comment)) {
+            deleteSshKey(i);
           }
         }
       }
-      db.accountSshKeys().delete(accountKeys);
     }
   }
 
-  private void deleteMail(Account.Id id, final String mailAddress)
-      throws UnloggedFailure, OrmException {
-    if (mailAddress.equals("ALL")) {
-      ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
-      for (AccountExternalId extId : ids) {
-        if (extId.isScheme(AccountExternalId.SCHEME_MAILTO)) {
-          unlink(id, extId.getEmailAddress());
-        }
+  private void deleteSshKey(SshKeyInfo i) throws OrmException {
+    AccountSshKey sshKey = new AccountSshKey(
+        new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
+    deleteSshKeyProvider.get().apply(
+        new AccountResource.SshKey(user, sshKey), null);
+  }
+
+  private void addEmail(String email) throws UnloggedFailure, RestApiException,
+      OrmException {
+    CreateEmail.Input in = new CreateEmail.Input();
+    in.email = email;
+    in.noConfirmation = true;
+    try {
+      createEmailFactory.create(email).apply(rsrc, in);
+    } catch (EmailException e) {
+      throw die(e.getMessage());
+    }
+  }
+
+  private void deleteEmail(String email) throws UnloggedFailure,
+      RestApiException, OrmException {
+    if (email.equals("ALL")) {
+      List<EmailInfo> emails = getEmailsProvider.get().apply(rsrc);
+      DeleteEmail deleteEmail = deleteEmailProvider.get();
+      for (EmailInfo e : emails) {
+        deleteEmail.apply(new AccountResource.Email(user, e.email),
+            new DeleteEmail.Input());
       }
     } else {
-      AccountExternalId.Key key = new AccountExternalId.Key(
-          AccountExternalId.SCHEME_MAILTO, mailAddress);
-      AccountExternalId extId = db.accountExternalIds().get(key);
-      if (extId != null) {
-        unlink(id, mailAddress);
-      }
-    }
-  }
-
-  private void setHttpPassword(Account.Id id, final String httpPassword)
-      throws UnloggedFailure, OrmException {
-    ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
-    for (AccountExternalId extId: ids) {
-      if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
-        extId.setPassword(httpPassword);
-        db.accountExternalIds().update(Collections.singleton(extId));
-        byIdCache.evict(id);
-      }
-    }
-  }
-
-  private void unlink(Account.Id id, final String mailAddress)
-      throws UnloggedFailure {
-    try {
-      manager.unlink(id, AuthRequest.forEmail(mailAddress));
-    } catch (AccountException ex) {
-      throw die(ex.getMessage());
-    }
-  }
-
-  private void link(Account.Id id, final String mailAddress)
-      throws UnloggedFailure {
-    try {
-      manager.link(id, AuthRequest.forEmail(mailAddress));
-    } catch (AccountException ex) {
-      throw die(ex.getMessage());
+      deleteEmailProvider.get().apply(new AccountResource.Email(user, email),
+          new DeleteEmail.Input());
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
new file mode 100644
index 0000000..48c37b8
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -0,0 +1,164 @@
+// 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.sshd.commands;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.AddIncludedGroups;
+import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.group.DeleteIncludedGroups;
+import com.google.gerrit.server.group.DeleteMembers;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+@CommandMetaData(name = "set-members", description = "Modifies members of specific group or number of groups")
+public class SetMembersCommand extends SshCommand {
+
+  @Option(name = "--add", aliases = {"-a"}, metaVar = "USER", usage = "users that should be added as group member")
+  private List<Account.Id> accountsToAdd = Lists.newArrayList();
+
+  @Option(name = "--remove", aliases = {"-r"}, metaVar = "USER", usage = "users that should be removed from the group")
+  private List<Account.Id> accountsToRemove = Lists.newArrayList();
+
+  @Option(name = "--include", aliases = {"-i"}, metaVar = "GROUP", usage = "group that should be included as group member")
+  private List<AccountGroup.UUID> groupsToInclude = Lists.newArrayList();
+
+  @Option(name = "--exclude", aliases = {"-e"}, metaVar = "GROUP", usage = "group that should be excluded from the group")
+  private List<AccountGroup.UUID> groupsToRemove = Lists.newArrayList();
+
+  @Argument(index = 0, required = true, multiValued = true, metaVar = "GROUP", usage = "groups to modify")
+  private List<AccountGroup.UUID> groups = Lists.newArrayList();
+
+  @Inject
+  private Provider<AddMembers> addMembers;
+
+  @Inject
+  private Provider<DeleteMembers> deleteMembers;
+
+  @Inject
+  private Provider<AddIncludedGroups> addIncludedGroups;
+
+  @Inject
+  private Provider<DeleteIncludedGroups> deleteIncludedGroups;
+
+  @Inject
+  private GroupsCollection groupsCollection;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
+  private AccountCache accountCache;
+
+  @Override
+  protected void run() throws UnloggedFailure, Failure, Exception {
+    for (AccountGroup.UUID groupUuid : groups) {
+      GroupResource resource =
+          groupsCollection.parse(TopLevelResource.INSTANCE,
+              IdString.fromUrl(groupUuid.get()));
+      if (!accountsToRemove.isEmpty()) {
+        deleteMembers.get().apply(resource, fromMembers(accountsToRemove));
+        reportMembersAction("removed from", resource, accountsToRemove);
+      }
+      if (!groupsToRemove.isEmpty()) {
+        deleteIncludedGroups.get().apply(resource, fromGroups(groupsToRemove));
+        reportGroupsAction("excluded from", resource, groupsToRemove);
+      }
+      if (!accountsToAdd.isEmpty()) {
+        addMembers.get().apply(resource, fromMembers(accountsToAdd));
+        reportMembersAction("added to", resource, accountsToAdd);
+      }
+      if (!groupsToInclude.isEmpty()) {
+        addIncludedGroups.get().apply(resource, fromGroups(groupsToInclude));
+        reportGroupsAction("included to", resource, groupsToInclude);
+      }
+    }
+  }
+
+  private void reportMembersAction(String action, GroupResource group,
+      List<Account.Id> accountIdList) throws UnsupportedEncodingException,
+      IOException {
+    out.write(String.format(
+        "Members %s group %s: %s\n",
+        action,
+        group.getName(),
+        Joiner.on(", ").join(
+            Iterables.transform(accountIdList,
+                new Function<Account.Id, String>() {
+                  @Override
+                  public String apply(Account.Id accountId) {
+                    return Objects.firstNonNull(accountCache.get(accountId)
+                        .getAccount().getPreferredEmail(), "n/a");
+                  }
+                }))).getBytes(ENC));
+  }
+
+  private void reportGroupsAction(String action, GroupResource group,
+      List<AccountGroup.UUID> groupUuidList)
+      throws UnsupportedEncodingException, IOException {
+    out.write(String.format(
+        "Groups %s group %s: %s\n",
+        action,
+        group.getName(),
+        Joiner.on(", ").join(
+            Iterables.transform(groupUuidList,
+                new Function<AccountGroup.UUID, String>() {
+                  @Override
+                  public String apply(AccountGroup.UUID uuid) {
+                    return groupCache.get(uuid).getName();
+                  }
+                }))).getBytes(ENC));
+  }
+
+  private AddIncludedGroups.Input fromGroups(List<AccountGroup.UUID> accounts) {
+    return AddIncludedGroups.Input.fromGroups(Lists.newArrayList(Iterables
+        .transform(accounts, new Function<AccountGroup.UUID, String>() {
+          @Override
+          public String apply(AccountGroup.UUID uuid) {
+            return uuid.toString();
+          }
+        })));
+  }
+
+  private AddMembers.Input fromMembers(List<Account.Id> accounts) {
+    return AddMembers.Input.fromMembers(Lists.newArrayList(Iterables.transform(
+        accounts, new Function<Account.Id, String>() {
+          @Override
+          public String apply(Account.Id id) {
+            return id.toString();
+          }
+        })));
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 8c06c97d..b45fb3a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -38,7 +38,7 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "set-project", descr = "Change a project's settings")
+@CommandMetaData(name = "set-project", description = "Change a project's settings")
 final class SetProjectCommand extends SshCommand {
   private static final Logger log = LoggerFactory
       .getLogger(SetProjectCommand.class);
@@ -108,6 +108,9 @@
   @Option(name = "--project-state", aliases = {"--ps"}, usage = "project's visibility state")
   private State state;
 
+  @Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
+  private String maxObjectSizeLimit;
+
   @Inject
   private MetaDataUpdate.User metaDataUpdateFactory;
 
@@ -148,6 +151,9 @@
         if (state != null) {
           project.setState(state);
         }
+        if (maxObjectSizeLimit != null) {
+          project.setMaxObjectSizeLimit(maxObjectSizeLimit);
+        }
         md.setMessage("Project settings updated");
         config.commit(md);
       } finally {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 8e9bcac..6dc79ff 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -45,7 +45,7 @@
 import java.util.List;
 import java.util.Set;
 
-@CommandMetaData(name = "set-reviewers", descr = "Add or remove reviewers on a change")
+@CommandMetaData(name = "set-reviewers", description = "Add or remove reviewers on a change")
 public class SetReviewersCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(SetReviewersCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index fbc0e75..ff1de80 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -21,10 +21,12 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.cache.h2.H2CacheImpl;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.inject.Inject;
@@ -52,14 +54,14 @@
 
 /** Show the current cache states. */
 @RequiresCapability(GlobalCapability.VIEW_CACHES)
-@CommandMetaData(name = "show-caches", descr = "Display current cache statistics")
+@CommandMetaData(name = "show-caches", description = "Display current cache statistics")
 final class ShowCaches extends CacheCommand {
   private static volatile long serverStarted;
 
   static class StartupListener implements LifecycleListener {
     @Override
     public void start() {
-      serverStarted = System.currentTimeMillis();
+      serverStarted = TimeUtil.nowMs();
     }
 
     @Override
@@ -209,13 +211,10 @@
 
   private Map<String, Cache<?, ?>> sortedPluginCaches() {
     SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
-    for (String plugin : cacheMap.plugins()) {
-      if ("gerrit".equals(plugin)) {
-        continue;
-      }
-      for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
-          cacheMap.byPlugin(plugin).entrySet()) {
-        m.put(cacheNameOf(plugin, entry.getKey()), entry.getValue().get());
+    for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+      if (!"gerrit".equals(e.getPluginName())) {
+        m.put(cacheNameOf(e.getPluginName(), e.getExportName()),
+            e.getProvider().get());
       }
     }
     return m;
@@ -273,7 +272,7 @@
       return;
     }
 
-    long now = System.currentTimeMillis();
+    long now = TimeUtil.nowMs();
     Collection<IoSession> list = acceptor.getManagedSessions().values();
     long oldest = now;
 
@@ -297,7 +296,7 @@
         runtimeBean.getVmVendor(),
         runtimeBean.getVmName(),
         runtimeBean.getVmVersion());
-    stdout.format("  on %s %s %s\n", "",
+    stdout.format("  on %s %s %s\n",
         osBean.getName(),
         osBean.getVersion(),
         osBean.getArch());
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 47ec5e4..d97d750 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
@@ -32,6 +33,7 @@
 import org.apache.sshd.server.session.ServerSession;
 import org.kohsuke.args4j.Option;
 
+import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
@@ -44,14 +46,33 @@
 
 /** Show the current SSH connections. */
 @RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
-@CommandMetaData(name = "show-connections", descr = "Display active client SSH connections")
+@CommandMetaData(name = "show-connections", description = "Display active client SSH connections")
 final class ShowConnections extends SshCommand {
   @Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
   private boolean numeric;
 
+  @Option(name = "--wide", aliases = {"-w"}, usage = "display without line width truncation")
+  private boolean wide;
+
   @Inject
   private SshDaemon daemon;
 
+  private int hostNameWidth;
+  private int columns = 80;
+
+  @Override
+  public void start(final Environment env) throws IOException {
+    String s = env.getEnv().get(Environment.ENV_COLUMNS);
+    if (s != null && !s.isEmpty()) {
+      try {
+        columns = Integer.parseInt(s);
+      } catch (NumberFormatException err) {
+        columns = 80;
+      }
+    }
+    super.start(env);
+ }
+
   @Override
   protected void run() throws Failure {
     final IoAcceptor acceptor = daemon.getIoAcceptor();
@@ -79,7 +100,9 @@
       }
     });
 
-    final long now = System.currentTimeMillis();
+    hostNameWidth = wide ? Integer.MAX_VALUE : columns - 9 - 9 - 10 - 32;
+
+    final long now = TimeUtil.nowMs();
     stdout.print(String.format("%-8s %8s %8s   %-15s %s\n", //
         "Session", "Start", "Idle", "User", "Remote Host"));
     stdout.print("--------------------------------------------------------------\n");
@@ -98,7 +121,7 @@
           ? now
           : now - minaSession.getSession().getLastIoTime();
 
-      stdout.print(String.format("%8s %8s %8s  %-15.15s %.30s\n", //
+      stdout.print(String.format("%8s %8s %8s   %-15.15s %s\n", //
           id(sd), //
           time(now, start), //
           age(idle), //
@@ -138,7 +161,7 @@
     }
 
     final CurrentUser user = sd.getCurrentUser();
-    if (user instanceof IdentifiedUser) {
+    if (user != null && user.isIdentifiedUser()) {
       IdentifiedUser u = (IdentifiedUser) user;
 
       if (!numeric) {
@@ -175,6 +198,11 @@
     if (host == null) {
       host = remoteAddress.toString();
     }
+
+    if (host.length() > hostNameWidth) {
+      return host.substring(0, hostNameWidth);
+    }
+
     return host;
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index fee5275..7759c94 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -40,9 +41,9 @@
 
 /** Display the current work queue. */
 @AdminHighPriorityCommand
-@CommandMetaData(name = "show-queue", descr = "Display the background work queues, including replication")
+@CommandMetaData(name = "show-queue", description = "Display the background work queues, including replication")
 final class ShowQueue extends SshCommand {
-  @Option(name = "-w", usage = "display without line width truncation")
+  @Option(name = "--wide", aliases = {"-w"}, usage = "display without line width truncation")
   private boolean wide;
 
   @Inject
@@ -94,15 +95,15 @@
       }
     });
 
-    taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 8 - 4;
+    taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4;
 
-    stdout.print(String.format("%-8s %-12s %-8s %s\n", //
-        "Task", "State", "", "Command"));
+    stdout.print(String.format("%-8s %-12s %-12s %-4s %s\n", //
+        "Task", "State", "StartTime", "", "Command"));
     stdout.print("----------------------------------------------"
         + "--------------------------------\n");
 
     int numberOfPendingTasks = 0;
-    final long now = System.currentTimeMillis();
+    final long now = TimeUtil.nowMs();
     final boolean viewAll = currentUser.getCapabilities().canViewQueue();
 
     for (final Task<?> task : pending) {
@@ -149,10 +150,12 @@
         }
       }
 
+      String startTime = startTime(task.getStartTime());
+
       // Shows information about tasks depending on the user rights
       if (viewAll || (!hasCustomizedPrint && regularUserCanSee)) {
-        stdout.print(String.format("%8s %-12s %-8s %s\n", //
-            id(task.getTaskId()), start, "", format(task)));
+        stdout.print(String.format("%8s %-12s %-12s %-4s %s\n", //
+            id(task.getTaskId()), start, startTime, "", format(task)));
       } else if (regularUserCanSee) {
         if (remoteName == null) {
           remoteName = projectName.get();
@@ -160,8 +163,8 @@
           remoteName = remoteName + "/" + projectName;
         }
 
-        stdout.print(String.format("%8s %-12s %-8s %s\n", //
-            id(task.getTaskId()), start, "", remoteName));
+        stdout.print(String.format("%8s %-12s %-4s %s\n", //
+            id(task.getTaskId()), start, startTime, remoteName));
       }
     }
     stdout.print("----------------------------------------------"
@@ -180,7 +183,15 @@
 
   private static String time(final long now, final long delay) {
     final Date when = new Date(now + delay);
-    if (delay < 24 * 60 * 60 * 1000L) {
+    return format(when, delay);
+  }
+
+  private static String startTime(final Date when) {
+    return format(when, TimeUtil.nowMs() - when.getTime());
+  }
+
+  private static String format(final Date when, final long timeFromNow) {
+    if (timeFromNow < 24 * 60 * 60 * 1000L) {
       return new SimpleDateFormat("HH:mm:ss.SSS").format(when);
     }
     return new SimpleDateFormat("MMM-dd HH:mm").format(when);
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 99d4baa..fd4a9ec 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
@@ -36,7 +36,7 @@
 import java.util.concurrent.LinkedBlockingQueue;
 
 @RequiresCapability(GlobalCapability.STREAM_EVENTS)
-@CommandMetaData(name = "stream-events", descr = "Monitor events occurring in real time")
+@CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time")
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
   private static final int MAX_EVENTS = 128;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
index 6335160..b957a7a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
@@ -23,7 +23,7 @@
 import com.google.inject.Provider;
 
 /** Command that allows testing of prolog submit-rules in a live instance. */
-@CommandMetaData(name = "rule", descr = "Test prolog submit rules")
+@CommandMetaData(name = "rule", description = "Test prolog submit rules")
 final class TestSubmitRuleCommand extends BaseTestPrologCommand {
   @Inject
   private Provider<TestSubmitRule> view;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
index 326ff46..2e7f0df 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
@@ -23,7 +23,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-@CommandMetaData(name = "type", descr = "Test prolog submit type")
+@CommandMetaData(name = "type", description = "Test prolog submit type")
 final class TestSubmitTypeCommand extends BaseTestPrologCommand {
   @Inject
   private Provider<TestSubmitType> view;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index 2066cc2..19888c8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 
-@CommandMetaData(name = "version", descr = "Display gerrit version")
+@CommandMetaData(name = "version", description = "Display gerrit version")
 final class VersionCommand extends SshCommand {
 
   @Override
diff --git a/gerrit-util-cli/BUCK b/gerrit-util-cli/BUCK
new file mode 100644
index 0000000..7e43cc4
--- /dev/null
+++ b/gerrit-util-cli/BUCK
@@ -0,0 +1,12 @@
+java_library(
+  name = 'cli',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-common:server',
+    '//lib:args4j',
+    '//lib:guava',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
deleted file mode 100644
index 9a897c7..0000000
--- a/gerrit-util-cli/pom.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-util-cli</artifactId>
-  <name>Gerrit Code Review - Utility - CLI</name>
-
-  <description>
-    Utilities to support command line parsing
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>args4j</groupId>
-      <artifactId>args4j</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.inject</groupId>
-      <artifactId>guice</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.inject.extensions</groupId>
-      <artifactId>guice-assistedinject</artifactId>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index 69598ce..b75635f 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -34,13 +34,12 @@
 
 package com.google.gerrit.util.cli;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Key;
 import com.google.inject.assistedinject.Assisted;
 
 import org.kohsuke.args4j.Argument;
@@ -75,7 +74,7 @@
     CmdLineParser create(Object bean);
   }
 
-  private final Injector injector;
+  private final OptionHandlers handlers;
   private final MyParser parser;
 
   @SuppressWarnings("rawtypes")
@@ -94,9 +93,9 @@
    *         annotations incorrectly.
    */
   @Inject
-  public CmdLineParser(final Injector injector, @Assisted final Object bean)
+  public CmdLineParser(OptionHandlers handlers, @Assisted final Object bean)
       throws IllegalAnnotationError {
-    this.injector = injector;
+    this.handlers = handlers;
     this.parser = new MyParser(bean);
   }
 
@@ -153,12 +152,7 @@
         }
         out.write('=');
 
-        String var = handler.getDefaultMetaVariable();
-        if (handler instanceof EnumOptionHandler) {
-          var = var.substring(1, var.length() - 1);
-          var = var.replaceAll(" ", "");
-        }
-        out.write(var);
+        out.write(metaVar(handler, n));
         if (!n.required()) {
           out.write(']');
         }
@@ -186,6 +180,17 @@
     }
   }
 
+  private static String metaVar(OptionHandler<?> handler, NamedOptionDef n) {
+    String var = n.metaVar();
+    if (Strings.isNullOrEmpty(var)) {
+      var = handler.getDefaultMetaVariable();
+      if (handler instanceof EnumOptionHandler) {
+        var = var.substring(1, var.length() - 1).replace(" ", "");
+      }
+    }
+    return var;
+  }
+
   public boolean wasHelpRequestedByOption() {
     return parser.help.value;
   }
@@ -327,16 +332,10 @@
         return add(super.createOptionHandler(option, setter));
       }
 
-      final Key<OptionHandlerFactory<?>> key =
-          OptionHandlerUtil.keyFor(setter.getType());
-      Injector i = injector;
-      while (i != null) {
-        if (i.getBindings().containsKey(key)) {
-          return add(i.getInstance(key).create(this, option, setter));
-        }
-        i = i.getParent();
+      OptionHandlerFactory<?> factory = handlers.get(setter.getType());
+      if (factory != null) {
+        return factory.create(this, option, setter);
       }
-
       return add(super.createOptionHandler(option, setter));
     }
 
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlers.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlers.java
new file mode 100644
index 0000000..756a885
--- /dev/null
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlers.java
@@ -0,0 +1,81 @@
+// 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.util.cli;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.Map.Entry;
+
+@Singleton
+public class OptionHandlers {
+  public static OptionHandlers empty() {
+    ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> m =
+        ImmutableMap.of();
+    return new OptionHandlers(m);
+  }
+
+  private final ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> map;
+
+  @Inject
+  OptionHandlers(Injector parent) {
+    this(build(parent));
+  }
+
+  OptionHandlers(ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> m) {
+    this.map = m;
+  }
+
+  @Nullable
+  OptionHandlerFactory<?> get(Class<?> type) {
+    Provider<OptionHandlerFactory<?>> b = map.get(type);
+    return b != null ? b.get() : null;
+  }
+
+  private static ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> build(Injector i) {
+    ImmutableMap.Builder<Class<?>, Provider<OptionHandlerFactory<?>>> map =
+        ImmutableMap.builder();
+    for (; i != null; i = i.getParent()) {
+      for (Entry<Key<?>, Binding<?>> e : i.getBindings().entrySet()) {
+        TypeLiteral<?> type = e.getKey().getTypeLiteral();
+        if (type.getRawType() == OptionHandlerFactory.class
+            && e.getKey().getAnnotation() == null
+            && type.getType() instanceof ParameterizedType) {
+          map.put(getType(type), cast(e.getValue()).getProvider());
+        }
+      }
+    }
+    return map.build();
+  }
+
+  private static Class<?> getType(TypeLiteral<?> t) {
+    ParameterizedType p = (ParameterizedType) t.getType();
+    return (Class<?>) p.getActualTypeArguments()[0];
+  }
+
+  private static Binding<OptionHandlerFactory<?>> cast(Binding<?> e) {
+    @SuppressWarnings("unchecked")
+    Binding<OptionHandlerFactory<?>> b = (Binding<OptionHandlerFactory<?>>) e;
+    return b;
+  }
+}
diff --git a/gerrit-util-ssl/BUCK b/gerrit-util-ssl/BUCK
new file mode 100644
index 0000000..068f34c
--- /dev/null
+++ b/gerrit-util-ssl/BUCK
@@ -0,0 +1,5 @@
+java_library(
+  name = 'ssl',
+  srcs = glob(['src/main/java/**/*.java']),
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
deleted file mode 100644
index e687912..0000000
--- a/gerrit-util-ssl/pom.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-util-ssl</artifactId>
-  <name>Gerrit Code Review - Utility - SSL</name>
-
-  <description>
-    Utilities to support SSL based protocols
-  </description>
-</project>
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
new file mode 100644
index 0000000..fa9a88f
--- /dev/null
+++ b/gerrit-war/BUCK
@@ -0,0 +1,75 @@
+include_defs('//tools/git.defs')
+
+java_library2(
+  name = 'init',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-extension-api:api',
+    '//gerrit-httpd:httpd',
+    '//gerrit-lucene:lucene',
+    '//gerrit-openid:openid',
+    '//gerrit-pgm:init-base',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-server/src/main/prolog:common',
+    '//gerrit-solr:solr',
+    '//gerrit-sshd:sshd',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
+    '//lib/log:api',
+    '//lib/jgit:jgit',
+  ],
+  compile_deps = ['//lib:servlet-api-3_0'],
+  visibility = [
+    '//:',
+    '//gerrit-gwtdebug:gwtdebug',
+    '//tools/eclipse:classpath',
+  ],
+)
+
+genrule(
+  name = 'webapp_assets',
+  cmd = 'cd $SRCDIR/src/main/webapp; zip -qr $OUT .',
+  srcs = glob(['src/main/webapp/**/*']),
+  deps = [],
+  out = 'webapp_assets.zip',
+  visibility = ['//:'],
+)
+
+genrule(
+  name = 'log4j-config__jar',
+  cmd = 'jar cf $OUT -C $SRCDIR/src/main/resources .',
+  srcs = ['src/main/resources/log4j.properties'],
+  out = 'log4j-config.jar',
+)
+
+prebuilt_jar(
+  name = 'log4j-config',
+  binary_jar = genfile('log4j-config.jar'),
+  deps = [':log4j-config__jar'],
+  visibility = [
+    '//:',
+    '//tools/eclipse:classpath',
+  ],
+)
+
+prebuilt_jar(
+  name = 'version',
+  binary_jar = genfile('version.jar'),
+  deps = [':gen_version'],
+  visibility = ['//:'],
+)
+
+genrule(
+  name = 'gen_version',
+  cmd = ';'.join([
+    'cd $TMP',
+    'mkdir -p com/google/gerrit/common',
+    'echo "%s" >com/google/gerrit/common/Version' % git_describe(),
+    'zip -9Dqr $OUT .',
+  ]),
+  out = 'version.jar',
+)
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
deleted file mode 100644
index b6c90e1..0000000
--- a/gerrit-war/pom.xml
+++ /dev/null
@@ -1,344 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>com.google.gerrit</groupId>
-    <artifactId>gerrit-parent</artifactId>
-    <version>2.7</version>
-  </parent>
-
-  <artifactId>gerrit-war</artifactId>
-  <name>Gerrit Code Review - WAR</name>
-  <packaging>war</packaging>
-
-  <description>
-    Gerrit packaged as a standard web application archive
-  </description>
-
-  <dependencies>
-    <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>tomcat-servlet-api</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-gwtui</artifactId>
-      <version>${project.version}</version>
-      <type>war</type>
-      <scope>runtime</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-main</artifactId>
-      <version>${project.version}</version>
-      <scope>runtime</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>bouncycastle</groupId>
-      <artifactId>bcprov-jdk15</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>bouncycastle</groupId>
-      <artifactId>bcpg-jdk15</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-log4j12</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>log4j</groupId>
-      <artifactId>log4j</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-openid</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-sshd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-httpd</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-pgm</artifactId>
-      <version>${project.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>org.eclipse.jetty</groupId>
-          <artifactId>jetty-servlet</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>org.eclipse.jetty</groupId>
-      <artifactId>jetty-servlet</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>servlet-api</artifactId>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-
-  <profiles>
-    <profile>
-      <id>plugins</id>
-      <activation>
-        <property>
-          <name>!gerrit.plugins.skip</name>
-        </property>
-      </activation>
-      <dependencies>
-        <!-- CORE PLUGIN LIST -->
-        <dependency>
-          <groupId>com.googlesource.gerrit.plugins.replication</groupId>
-          <artifactId>replication</artifactId>
-          <version>${project.version}</version>
-          <scope>provided</scope>
-        </dependency>
-        <dependency>
-          <groupId>com.googlesource.gerrit.plugins.reviewnotes</groupId>
-          <artifactId>reviewnotes</artifactId>
-          <version>${project.version}</version>
-          <scope>provided</scope>
-        </dependency>
-        <dependency>
-          <groupId>com.googlesource.gerrit.plugins.validators</groupId>
-          <artifactId>commit-message-length-validator</artifactId>
-          <version>${project.version}</version>
-          <scope>provided</scope>
-        </dependency>
-      </dependencies>
-    </profile>
-  </profiles>
-
-  <build>
-	<pluginManagement>
-	  <plugins>
-	    <plugin>
-	      <groupId>org.eclipse.m2e</groupId>
-	      <artifactId>lifecycle-mapping</artifactId>
-	      <version>1.0.0</version>
-	      <configuration>
-	        <lifecycleMappingMetadata>
-	          <pluginExecutions>
-	            <pluginExecution>
-	              <pluginExecutionFilter>
-	                <groupId>org.apache.maven.plugins</groupId>
-	                <artifactId>maven-dependency-plugin</artifactId>
-	                <versionRange>[2.0,)</versionRange>
-	                <goals>
-	                  <goal>copy-dependencies</goal>
-	                  <goal>unpack</goal>goal>
-	                </goals>
-	              </pluginExecutionFilter>
-	              <action>
-	                <execute />
-	              </action>
-	            </pluginExecution>
-	          </pluginExecutions>
-	        </lifecycleMappingMetadata>
-	      </configuration>
-	    </plugin>
-	  </plugins>
-	</pluginManagement>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-war-plugin</artifactId>
-        <configuration>
-          <warName>gerrit-${project.version}</warName>
-          <archiveClasses>true</archiveClasses>
-          <attachClasses>true</attachClasses>
-          <archive>
-            <addMavenDescriptor>false</addMavenDescriptor>
-            <manifestEntries>
-              <Main-Class>Main</Main-Class>
-              <Implementation-Title>Gerrit Code Review</Implementation-Title>
-              <Implementation-Version>${project.version}</Implementation-Version>
-            </manifestEntries>
-          </archive>
-          <overlays>
-            <overlay>
-              <groupId>com.google.gerrit</groupId>
-              <artifactId>gerrit-main</artifactId>
-              <type>jar</type>
-              <includes>
-                <include>Main.class</include>
-                <include>com/google/gerrit/launcher/*.class</include>
-              </includes>
-            </overlay>
-          </overlays>
-        </configuration>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-dependency-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>copy-servlet-api</id>
-            <configuration>
-              <includeGroupIds>org.apache.tomcat,org.eclipse.jetty</includeGroupIds>
-              <excludeArtifactIds>servlet-api</excludeArtifactIds>
-            </configuration>
-            <goals>
-              <goal>copy-dependencies</goal>
-            </goals>
-          </execution>
-          <execution>
-            <id>copy-plugins</id>
-            <configuration>
-              <!-- CORE PLUGIN LIST -->
-              <includeArtifactIds>commit-message-length-validator,replication,reviewnotes</includeArtifactIds>
-              <includeTypes>jar</includeTypes>
-              <stripVersion>true</stripVersion>
-              <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/plugins</outputDirectory>
-            </configuration>
-            <goals>
-              <goal>copy-dependencies</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-antrun-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>copy-servlet-api</id>
-            <phase>process-classes</phase>
-            <configuration>
-              <target>
-                <property name="src" location="${project.build.directory}/dependency" />
-                <property name="dst" location="${project.build.directory}/${project.build.finalName}/WEB-INF/pgm-lib" />
-
-                <mkdir dir="${dst}" />
-                <copy overwrite="true" todir="${dst}">
-                  <fileset dir="${src}">
-                    <include name="*.jar" />
-                  </fileset>
-                </copy>
-              </target>
-            </configuration>
-            <goals>
-              <goal>run</goal>
-            </goals>
-          </execution>
-          <execution>
-            <id>copy-license</id>
-            <phase>process-classes</phase>
-            <configuration>
-              <target>
-                <property name="src" location="${basedir}/../Documentation" />
-                <property name="dst" location="${project.build.directory}/${project.build.finalName}" />
-
-                <copy tofile="${dst}/LICENSES.txt"
-                      file="${src}/licenses.txt"
-                      overwrite="true" />
-              </target>
-            </configuration>
-            <goals>
-              <goal>run</goal>
-            </goals>
-          </execution>
-          <execution>
-            <id>include-documentation</id>
-            <phase>process-classes</phase>
-            <configuration>
-              <target unless="gerrit.documentation.skip">
-                <property name="src" location="${basedir}/../Documentation" />
-                <property name="out" location="${project.build.directory}/${project.build.finalName}" />
-                <property name="dst" location="${out}/Documentation" />
-
-                <exec dir="${src}" executable="make">
-                  <arg value="VERSION=${project.version}" />
-                  <arg value="clean" />
-                  <arg value="all" />
-                </exec>
-
-                <mkdir dir="${dst}" />
-                <copy overwrite="true" todir="${dst}">
-                  <fileset dir="${src}">
-                    <include name="*.html" />
-                  </fileset>
-                </copy>
-              </target>
-            </configuration>
-            <goals>
-              <goal>run</goal>
-            </goals>
-          </execution>
-          <execution>
-            <id>include-release-notes</id>
-            <phase>process-classes</phase>
-            <configuration>
-              <target unless="gerrit.documentation.skip">
-                <property name="src" location="${basedir}/../ReleaseNotes" />
-                <property name="out" location="${project.build.directory}/${project.build.finalName}" />
-                <property name="dst" location="${out}/ReleaseNotes" />
-
-                <exec dir="${src}" executable="make">
-                  <arg value="VERSION=${project.version}" />
-                  <arg value="clean" />
-                  <arg value="all" />
-                </exec>
-
-                <mkdir dir="${dst}" />
-                <copy overwrite="true" todir="${dst}">
-                  <fileset dir="${src}">
-                    <include name="*.html" />
-                  </fileset>
-                </copy>
-              </target>
-            </configuration>
-            <goals>
-              <goal>run</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
new file mode 100644
index 0000000..0a9386d
--- /dev/null
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import com.google.gerrit.pgm.BaseInit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public final class SiteInitializer {
+  private static final Logger LOG = LoggerFactory
+      .getLogger(SiteInitializer.class);
+
+  private final String sitePath;
+  private final String initPath;
+
+  SiteInitializer(String sitePath, String initPath) {
+    this.sitePath = sitePath;
+    this.initPath = initPath;
+  }
+
+  public void init() {
+    try {
+      if (sitePath != null) {
+        File site = new File(sitePath);
+        LOG.info(String.format("Initializing site at %s",
+            site.getAbsolutePath()));
+        new BaseInit(site, false).run();
+        return;
+      }
+
+      Connection conn = connectToDb();
+      try {
+        File site = getSiteFromReviewDb(conn);
+        if (site == null && initPath != null) {
+          site = new File(initPath);
+        }
+        if (site != null) {
+          LOG.info(String.format("Initializing site at %s",
+              site.getAbsolutePath()));
+          new BaseInit(site, new ReviewDbDataSourceProvider(), false).run();
+        }
+      } finally {
+        conn.close();
+      }
+    } catch (Exception e) {
+      LOG.error("Site init failed", e);
+      throw new RuntimeException(e);
+    }
+  }
+
+  private Connection connectToDb() throws SQLException {
+    return new ReviewDbDataSourceProvider().get().getConnection();
+  }
+
+  private File getSiteFromReviewDb(Connection conn) {
+    try {
+      Statement stmt = conn.createStatement();
+      try {
+        ResultSet rs = stmt.executeQuery("SELECT site_path FROM system_config");
+        if (rs.next()) {
+          return new File(rs.getString(1));
+        }
+      } finally {
+        stmt.close();
+      }
+      return null;
+    } catch (SQLException e) {
+      return null;
+    }
+  }
+}
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 945547f..5936911 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
@@ -18,12 +18,13 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.httpd.GerritUiOptions;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
@@ -37,17 +38,21 @@
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.NoIndexModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.patch.IntraLineWorkerPool;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
-import com.google.gerrit.server.plugins.PluginModule;
+import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.schema.DataSourceModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.solr.SolrIndexModule;
+import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
@@ -111,6 +116,10 @@
         sitePath = new File(path);
       }
 
+      if (System.getProperty("gerrit.init") != null) {
+        new SiteInitializer(path, System.getProperty("gerrit.init_path")).init();
+      }
+
       try {
         dbInjector = createDbInjector();
       } catch (CreationException ce) {
@@ -249,10 +258,23 @@
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new PluginModule());
+    modules.add(new PluginRestApiModule());
+    AbstractModule changeIndexModule;
+    switch (IndexModule.getIndexType(cfgInjector)) {
+      case LUCENE:
+        changeIndexModule = new LuceneIndexModule();
+        break;
+      case SOLR:
+        changeIndexModule = new SolrIndexModule();
+        break;
+      default:
+        changeIndexModule = new NoIndexModule();
+    }
+    modules.add(changeIndexModule);
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
@@ -273,6 +295,7 @@
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<Module>();
     modules.add(sysInjector.getInstance(SshModule.class));
+    modules.add(new SshHostKeyModule());
     modules.add(new MasterCommandModule());
     return sysInjector.createChildInjector(modules);
   }
diff --git a/lib/BUCK b/lib/BUCK
new file mode 100644
index 0000000..c9549b3
--- /dev/null
+++ b/lib/BUCK
@@ -0,0 +1,251 @@
+include_defs('//lib/maven.defs')
+
+define_license(name = 'Apache1.1')
+define_license(name = 'Apache2.0')
+define_license(name = 'CC-BY3.0')
+define_license(name = 'MPL1.1')
+define_license(name = 'PublicDomain')
+define_license(name = 'antlr')
+define_license(name = 'args4j')
+define_license(name = 'automaton')
+define_license(name = 'bouncycastle')
+define_license(name = 'clippy')
+define_license(name = 'codemirror')
+define_license(name = 'diffy')
+define_license(name = 'h2')
+define_license(name = 'jgit')
+define_license(name = 'jsch')
+define_license(name = 'ow2')
+define_license(name = 'postgresql')
+define_license(name = 'prologcafe')
+define_license(name = 'protobuf')
+define_license(name = 'slf4j')
+define_license(name = 'DO_NOT_DISTRIBUTE')
+
+maven_jar(
+  name = 'gwtorm',
+  id = 'gwtorm:gwtorm:1.7',
+  bin_sha1 = 'ee3b316a023f1422dd4b6654a3d51d0e5690809c',
+  src_sha1 = 'a145bde4cc87a4ff4cec283880e2a03be32cc868',
+  license = 'Apache2.0',
+  deps = [':protobuf'],
+  repository = GERRIT,
+)
+
+maven_jar(
+  name = 'gwtjsonrpc',
+  id = 'gwtjsonrpc:gwtjsonrpc:1.3',
+  bin_sha1 = '1717ba11ab0c5160798c80085220a63f864691d3',
+  src_sha1 = '9e01c5d7bd54f8e70066450b372a43c16404789e',
+  license = 'Apache2.0',
+  repository = GERRIT,
+)
+
+maven_jar(
+  name = 'gson',
+  id = 'com.google.code.gson:gson:2.1',
+  sha1 = '2e66da15851f9f5b5079228f856c2f090ba98c38',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'guava',
+  id = 'com.google.guava:guava:15.0',
+  sha1 = 'ed727a8d9f247e2050281cb083f1c77b09dcb5cd',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'asm3',
+  id = 'asm:asm:3.2',
+  sha1 = '9bc1511dec6adf302991ced13303e4140fdf9ab7',
+  license = 'ow2',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'ow2-asm',
+  id = 'org.ow2.asm:asm:4.0',
+  sha1 = '659add6efc75a4715d738e73f07505246edf4d66',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-analysis',
+  id = 'org.ow2.asm:asm-analysis:4.0',
+  sha1 = '1c45d52b6f6c638db13cf3ac12adeb56b254cdd7',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-tree',
+  id = 'org.ow2.asm:asm-tree:4.0',
+  sha1 = '67bd266cd17adcee486b76952ece4cc85fe248b8',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-util',
+  id = 'org.ow2.asm:asm-util:4.0',
+  sha1 = 'd7a65f54cda284f9706a750c23d64830bb740c39',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'velocity',
+  id = 'org.apache.velocity:velocity:1.6.4',
+  sha1 = 'fcc58693dd8fc83d714fba149789be37cc19b66d',
+  license = 'Apache2.0',
+  deps = [
+    '//lib/commons:collections',
+    '//lib/commons:lang',
+    '//lib/commons:oro',
+  ],
+  exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
+)
+
+maven_jar(
+  name = 'jsch',
+  id = 'com.jcraft:jsch:0.1.50',
+  sha1 = 'fae4a0b1f2a96cb8f58f38da2650814c991cea01',
+  license = 'jsch',
+)
+
+maven_jar(
+  name = 'servlet-api-3_0',
+  id = 'org.apache.tomcat:tomcat-servlet-api:7.0.32',
+  sha1 = 'e2f21e9868414122e6dd23ac66cf304d4290642c',
+  license = 'Apache2.0',
+  exclude = ['META-INF/NOTICE', 'META-INF/LICENSE'],
+)
+
+maven_jar(
+  name = 'args4j',
+  id = 'args4j:args4j:2.0.16',
+  sha1 = '9f00fb12820743b9e05c686eba543d64dd43f2b1',
+  license = 'args4j',
+)
+
+maven_jar(
+  name = 'mime-util',
+  id = 'eu.medsea.mimeutil:mime-util:2.1.3',
+  sha1 = '0c9cfae15c74f62491d4f28def0dff1dabe52a47',
+  license = 'Apache2.0',
+  exclude = ['LICENSE.txt', 'README.txt'],
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'juniversalchardet',
+  id = 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3',
+  sha1 = 'cd49678784c46aa8789c060538e0154013bb421b',
+  license = 'MPL1.1',
+)
+
+maven_jar(
+  name = 'automaton',
+  id = 'dk.brics.automaton:automaton:1.11-8',
+  sha1 = '6ebfa65eb431ff4b715a23be7a750cbc4cc96d0f',
+  license = 'automaton',
+)
+
+maven_jar(
+  name = 'pegdown',
+  id = 'org.pegdown:pegdown:1.1.0',
+  sha1 = '00bcc0c5b025b09ab85bb80a8311ce5c015d005b',
+  license = 'Apache2.0',
+  deps = [':parboiled-java'],
+  exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
+)
+
+maven_jar(
+  name = 'parboiled-core',
+  id = 'org.parboiled:parboiled-core:1.1.3',
+  sha1 = '3fc3013adf98701efcc594a1ea99a3f841dc81bb',
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'parboiled-java',
+  id = 'org.parboiled:parboiled-java:1.1.3',
+  sha1 = 'c2bf2935a8b3eca5f998557190cd6eb34f5536d0',
+  license = 'Apache2.0',
+  deps = [
+    ':parboiled-core',
+    ':ow2-asm-tree',
+    ':ow2-asm-analysis',
+    ':ow2-asm-util',
+  ],
+  attach_source = False,
+  visibility = [],
+)
+
+maven_jar(
+  name = 'h2',
+  id = 'com.h2database:h2:1.3.173',
+  sha1 = '3d9cc700d2c6b0b7a9bb59bd2b850e6518b6c209',
+  license = 'h2',
+)
+
+maven_jar(
+  name = 'postgresql',
+  id = 'postgresql:postgresql:9.1-901-1.jdbc4',
+  sha1 = '9bfabe48876ec38f6cbaa6931bad05c64a9ea942',
+  license = 'postgresql',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'protobuf',
+  # Must match version in gwtorm/pom.xml.
+  id = 'com.google.protobuf:protobuf-java:2.4.1',
+  bin_sha1 = '0c589509ec6fd86d5d2fda37e07c08538235d3b9',
+  src_sha1 = 'e406f69360f2a89cb4aa724ed996a1c5599af383',
+  license = 'protobuf',
+)
+
+maven_jar(
+  name = 'junit',
+  id = 'junit:junit:4.11',
+  sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [':hamcrest-core'],
+)
+
+maven_jar(
+  name = 'hamcrest-core',
+  id = 'org.hamcrest:hamcrest-core:1.3',
+  sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = ['//lib:junit'],
+)
+
+maven_jar(
+  name = 'easymock',
+  id = 'org.easymock:easymock:3.1',
+  sha1 = '3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':cglib-2_2',
+    ':objenesis',
+  ],
+)
+
+maven_jar(
+  name = 'cglib-2_2',
+  id = 'cglib:cglib-nodep:2.2.2',
+  sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = ['//lib:easymock'],
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'objenesis',
+  id = 'org.objenesis:objenesis:1.2',
+  sha1 = 'bfcb0539a071a4c5a30690388903ac48c0667f2a',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = ['//lib:easymock'],
+  attach_source = False,
+)
diff --git a/lib/LICENSE-Apache1.1 b/lib/LICENSE-Apache1.1
new file mode 100644
index 0000000..8eda4fc
--- /dev/null
+++ b/lib/LICENSE-Apache1.1
@@ -0,0 +1,51 @@
+The Apache Software License, Version 1.1
+
+Copyright (c) 2000-2002 The Apache Software Foundation.  All rights
+reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in
+   the documentation and/or other materials provided with the
+   distribution.
+
+3. The end-user documentation included with the redistribution,
+   if any, must include the following acknowledgment:
+      "This product includes software developed by the
+       Apache Software Foundation (http://www.apache.org/)."
+   Alternately, this acknowledgment may appear in the software itself,
+   if and wherever such third-party acknowledgments normally appear.
+
+4. The names "Apache" and "Apache Software Foundation", "Jakarta-Oro"
+   must not be used to endorse or promote products derived from this
+   software without prior written permission. For written
+   permission, please contact apache@apache.org.
+
+5. Products derived from this software may not be called "Apache"
+   or "Jakarta-Oro", nor may "Apache" or "Jakarta-Oro" appear in their
+   name, without prior written permission of the Apache Software Foundation.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+====================================================================
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the Apache Software Foundation.  For more
+information on the Apache Software Foundation, please see
+<http://www.apache.org/>.
diff --git a/lib/LICENSE-Apache2.0 b/lib/LICENSE-Apache2.0
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/lib/LICENSE-Apache2.0
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/lib/LICENSE-CC-BY3.0 b/lib/LICENSE-CC-BY3.0
new file mode 100644
index 0000000..39dbc91
--- /dev/null
+++ b/lib/LICENSE-CC-BY3.0
@@ -0,0 +1,333 @@
+link:http://creativecommons.org/licenses/by/3.0/us/[CC-BY 3.0]
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
+CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE").  THE WORK IS
+PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW.  ANY USE OF THE
+WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
+PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
+AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE.  TO THE EXTENT THIS
+LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
+THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH
+TERMS AND CONDITIONS.
+
+1.  Definitions
+
+  a.  "Adaptation" means a work based upon the Work, or upon the Work
+      and other pre-existing works, such as a translation, adaptation,
+      derivative work, arrangement of music or other alterations of a
+      literary or artistic work, or phonogram or performance and
+      includes cinematographic adaptations or any other form in which
+      the Work may be recast, transformed, or adapted including in any
+      form recognizably derived from the original, except that a work
+      that constitutes a Collection will not be considered an
+      Adaptation for the purpose of this License.  For the avoidance
+      of doubt, where the Work is a musical work, performance or
+      phonogram, the synchronization of the Work in timed-relation
+      with a moving image ("synching") will be considered an
+      Adaptation for the purpose of this License.
+
+  b.  "Collection" means a collection of literary or artistic works,
+      such as encyclopedias and anthologies, or performances,
+      phonograms or broadcasts, or other works or subject matter other
+      than works listed in Section 1(f) below, which, by reason of the
+      selection and arrangement of their contents, constitute
+      intellectual creations, in which the Work is included in its
+      entirety in unmodified form along with one or more other
+      contributions, each constituting separate and independent works
+      in themselves, which together are assembled into a collective
+      whole.  A work that constitutes a Collection will not be
+      considered an Adaptation (as defined above) for the purposes of
+      this License.
+
+  c.  "Distribute" means to make available to the public the original
+      and copies of the Work or Adaptation, as appropriate, through
+      sale or other transfer of ownership.
+
+  d.  "Licensor" means the individual, individuals, entity or entities
+      that offer(s) the Work under the terms of this License.
+
+  e.  "Original Author" means, in the case of a literary or artistic
+      work, the individual, individuals, entity or entities who
+      created the Work or if no individual or entity can be
+      identified, the publisher; and in addition (i) in the case of a
+      performance the actors, singers, musicians, dancers, and other
+      persons who act, sing, deliver, declaim, play in, interpret or
+      otherwise perform literary or artistic works or expressions of
+      folklore; (ii) in the case of a phonogram the producer being the
+      person or legal entity who first fixes the sounds of a
+      performance or other sounds; and, (iii) in the case of
+      broadcasts, the organization that transmits the broadcast.
+
+  f.  "Work" means the literary and/or artistic work offered under the
+      terms of this License including without limitation any
+      production in the literary, scientific and artistic domain,
+      whatever may be the mode or form of its expression including
+      digital form, such as a book, pamphlet and other writing; a
+      lecture, address, sermon or other work of the same nature; a
+      dramatic or dramatico-musical work; a choreographic work or
+      entertainment in dumb show; a musical composition with or
+      without words; a cinematographic work to which are assimilated
+      works expressed by a process analogous to cinematography; a work
+      of drawing, painting, architecture, sculpture, engraving or
+      lithography; a photographic work to which are assimilated works
+      expressed by a process analogous to photography; a work of
+      applied art; an illustration, map, plan, sketch or
+      three-dimensional work relative to geography, topography,
+      architecture or science; a performance; a broadcast; a
+      phonogram; a compilation of data to the extent it is protected
+      as a copyrightable work; or a work performed by a variety or
+      circus performer to the extent it is not otherwise considered a
+      literary or artistic work.
+
+  g.  "You" means an individual or entity exercising rights under this
+      License who has not previously violated the terms of this
+      License with respect to the Work, or who has received express
+      permission from the Licensor to exercise rights under this
+      License despite a previous violation.
+
+  h.  "Publicly Perform" means to perform public recitations of the
+      Work and to communicate to the public those public recitations,
+      by any means or process, including by wire or wireless means or
+      public digital performances; to make available to the public
+      Works in such a way that members of the public may access these
+      Works from a place and at a place individually chosen by them;
+      to perform the Work to the public by any means or process and
+      the communication to the public of the performances of the Work,
+      including by public digital performance; to broadcast and
+      rebroadcast the Work by any means including signs, sounds or
+      images.
+
+  i.  "Reproduce" means to make copies of the Work by any means
+      including without limitation by sound or visual recordings and
+      the right of fixation and reproducing fixations of the Work,
+      including storage of a protected performance or phonogram in
+      digital form or other electronic medium.
+
+2.  Fair Dealing Rights.  Nothing in this License is intended to
+    reduce, limit, or restrict any uses free from copyright or rights
+    arising from limitations or exceptions that are provided for in
+    connection with the copyright protection under copyright law or
+    other applicable laws.
+
+3.  License Grant.  Subject to the terms and conditions of this
+    License, Licensor hereby grants You a worldwide, royalty-free,
+    non-exclusive, perpetual (for the duration of the applicable
+    copyright) license to exercise the rights in the Work as stated
+    below:
+
+  a.  to Reproduce the Work, to incorporate the Work into one or more
+      Collections, and to Reproduce the Work as incorporated in the
+      Collections;
+
+  b.  to create and Reproduce Adaptations provided that any such
+      Adaptation, including any translation in any medium, takes
+      reasonable steps to clearly label, demarcate or otherwise
+      identify that changes were made to the original Work.  For
+      example, a translation could be marked "The original work was
+      translated from English to Spanish," or a modification could
+      indicate "The original work has been modified.";
+
+  c.  to Distribute and Publicly Perform the Work including as
+      incorporated in Collections; and,
+
+  d.  to Distribute and Publicly Perform Adaptations.
+
+  e.  For the avoidance of doubt:
+
+    i.   Non-waivable Compulsory License Schemes.  In those
+	     jurisdictions in which the right to collect royalties
+	     through any statutory or compulsory licensing scheme
+	     cannot be waived, the Licensor reserves the exclusive
+	     right to collect such royalties for any exercise by You
+	     of the rights granted under this License;
+
+    ii.  Waivable Compulsory License Schemes.  In those jurisdictions
+	     in which the right to collect royalties through any
+	     statutory or compulsory licensing scheme can be waived,
+	     the Licensor waives the exclusive right to collect such
+	     royalties for any exercise by You of the rights granted
+	     under this License; and,
+
+    iii. Voluntary License Schemes.  The Licensor waives the right to
+	     collect royalties, whether individually or, in the event
+	     that the Licensor is a member of a collecting society
+	     that administers voluntary licensing schemes, via that
+	     society, from any exercise by You of the rights granted
+	     under this License.
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised.  The above rights include the right to
+make such modifications as are technically necessary to exercise the
+rights in other media and formats.  Subject to Section 8(f), all
+rights not expressly granted by Licensor are hereby reserved.
+
+4.  Restrictions.  The license granted in Section 3 above is expressly
+    made subject to and limited by the following restrictions:
+
+  a.  You may Distribute or Publicly Perform the Work only under the
+      terms of this License.  You must include a copy of, or the
+      Uniform Resource Identifier (URI) for, this License with every
+      copy of the Work You Distribute or Publicly Perform.  You may
+      not offer or impose any terms on the Work that restrict the
+      terms of this License or the ability of the recipient of the
+      Work to exercise the rights granted to that recipient under the
+      terms of the License.  You may not sublicense the Work.  You
+      must keep intact all notices that refer to this License and to
+      the disclaimer of warranties with every copy of the Work You
+      Distribute or Publicly Perform.  When You Distribute or Publicly
+      Perform the Work, You may not impose any effective technological
+      measures on the Work that restrict the ability of a recipient of
+      the Work from You to exercise the rights granted to that
+      recipient under the terms of the License.  This Section 4(a)
+      applies to the Work as incorporated in a Collection, but this
+      does not require the Collection apart from the Work itself to be
+      made subject to the terms of this License.  If You create a
+      Collection, upon notice from any Licensor You must, to the
+      extent practicable, remove from the Collection any credit as
+      required by Section 4(b), as requested.  If You create an
+      Adaptation, upon notice from any Licensor You must, to the
+      extent practicable, remove from the Adaptation any credit as
+      required by Section 4(b), as requested.
+
+  b.  If You Distribute, or Publicly Perform the Work or any
+      Adaptations or Collections, You must, unless a request has been
+      made pursuant to Section 4(a), keep intact all copyright notices
+      for the Work and provide, reasonable to the medium or means You
+      are utilizing: (i) the name of the Original Author (or
+      pseudonym, if applicable) if supplied, and/or if the Original
+      Author and/or Licensor designate another party or parties (e.g.,
+      a sponsor institute, publishing entity, journal) for attribution
+      ("Attribution Parties") in Licensor's copyright notice, terms of
+      service or by other reasonable means, the name of such party or
+      parties; (ii) the title of the Work if supplied; (iii) to the
+      extent reasonably practicable, the URI, if any, that Licensor
+      specifies to be associated with the Work, unless such URI does
+      not refer to the copyright notice or licensing information for
+      the Work; and (iv) , consistent with Section 3(b), in the case
+      of an Adaptation, a credit identifying the use of the Work in
+      the Adaptation (e.g., "French translation of the Work by
+      Original Author," or "Screenplay based on original Work by
+      Original Author").  The credit required by this Section 4 (b)
+      may be implemented in any reasonable manner; provided, however,
+      that in the case of a Adaptation or Collection, at a minimum
+      such credit will appear, if a credit for all contributing
+      authors of the Adaptation or Collection appears, then as part of
+      these credits and in a manner at least as prominent as the
+      credits for the other contributing authors.  For the avoidance
+      of doubt, You may only use the credit required by this Section
+      for the purpose of attribution in the manner set out above and,
+      by exercising Your rights under this License, You may not
+      implicitly or explicitly assert or imply any connection with,
+      sponsorship or endorsement by the Original Author, Licensor
+      and/or Attribution Parties, as appropriate, of You or Your use
+      of the Work, without the separate, express prior written
+      permission of the Original Author, Licensor and/or Attribution
+      Parties.
+
+  c.  Except as otherwise agreed in writing by the Licensor or as may
+      be otherwise permitted by applicable law, if You Reproduce,
+      Distribute or Publicly Perform the Work either by itself or as
+      part of any Adaptations or Collections, You must not distort,
+      mutilate, modify or take other derogatory action in relation to
+      the Work which would be prejudicial to the Original Author's
+      honor or reputation.  Licensor agrees that in those
+      jurisdictions (e.g.  Japan), in which any exercise of the right
+      granted in Section 3(b) of this License (the right to make
+      Adaptations) would be deemed to be a distortion, mutilation,
+      modification or other derogatory action prejudicial to the
+      Original Author's honor and reputation, the Licensor will waive
+      or not assert, as appropriate, this Section, to the fullest
+      extent permitted by the applicable national law, to enable You
+      to reasonably exercise Your right under Section 3(b) of this
+      License (right to make Adaptations) but not otherwise.
+
+5.  Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
+LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
+WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
+STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
+TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
+NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
+OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE.
+SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES,
+SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6.  Limitation on Liability.  EXCEPT TO THE EXTENT REQUIRED BY
+    APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
+    LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
+    OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE
+    WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+    DAMAGES.
+
+7.  Termination
+
+  a.  This License and the rights granted hereunder will terminate
+      automatically upon any breach by You of the terms of this
+      License.  Individuals or entities who have received Adaptations
+      or Collections from You under this License, however, will not
+      have their licenses terminated provided such individuals or
+      entities remain in full compliance with those licenses.
+      Sections 1, 2, 5, 6, 7, and 8 will survive any termination of
+      this License.
+
+  b.  Subject to the above terms and conditions, the license granted
+      here is perpetual (for the duration of the applicable copyright
+      in the Work).  Notwithstanding the above, Licensor reserves the
+      right to release the Work under different license terms or to
+      stop distributing the Work at any time; provided, however that
+      any such election will not serve to withdraw this License (or
+      any other license that has been, or is required to be, granted
+      under the terms of this License), and this License will continue
+      in full force and effect unless terminated as stated above.
+
+8. Miscellaneous
+
+  a.  Each time You Distribute or Publicly Perform the Work or a
+      Collection, the Licensor offers to the recipient a license to
+      the Work on the same terms and conditions as the license granted
+      to You under this License.
+
+  b.  Each time You Distribute or Publicly Perform an Adaptation,
+      Licensor offers to the recipient a license to the original Work
+      on the same terms and conditions as the license granted to You
+      under this License.
+
+  c.  If any provision of this License is invalid or unenforceable
+      under applicable law, it shall not affect the validity or
+      enforceability of the remainder of the terms of this License,
+      and without further action by the parties to this agreement,
+      such provision shall be reformed to the minimum extent necessary
+      to make such provision valid and enforceable.
+
+  d.  No term or provision of this License shall be deemed waived and
+      no breach consented to unless such waiver or consent shall be in
+      writing and signed by the party to be charged with such waiver
+      or consent.
+
+  e.  This License constitutes the entire agreement between the
+      parties with respect to the Work licensed here.  There are no
+      understandings, agreements or representations with respect to
+      the Work not specified here.  Licensor shall not be bound by any
+      additional provisions that may appear in any communication from
+      You.  This License may not be modified without the mutual
+      written agreement of the Licensor and You.
+
+  f.  The rights granted under, and the subject matter referenced, in
+      this License were drafted utilizing the terminology of the Berne
+      Convention for the Protection of Literary and Artistic Works (as
+      amended on September 28, 1979), the Rome Convention of 1961, the
+      WIPO Copyright Treaty of 1996, the WIPO Performances and
+      Phonograms Treaty of 1996 and the Universal Copyright Convention
+      (as revised on July 24, 1971).  These rights and subject matter
+      take effect in the relevant jurisdiction in which the License
+      terms are sought to be enforced according to the corresponding
+      provisions of the implementation of those treaty provisions in
+      the applicable national law.  If the standard suite of rights
+      granted under applicable copyright law includes additional
+      rights not granted under this License, such additional rights
+      are deemed to be included in the License; this License is not
+      intended to restrict the license of any rights under applicable
+      law.
diff --git a/lib/LICENSE-DO_NOT_DISTRIBUTE b/lib/LICENSE-DO_NOT_DISTRIBUTE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/LICENSE-DO_NOT_DISTRIBUTE
diff --git a/lib/LICENSE-MPL1.1 b/lib/LICENSE-MPL1.1
new file mode 100644
index 0000000..06f9651
--- /dev/null
+++ b/lib/LICENSE-MPL1.1
@@ -0,0 +1,469 @@
+                          MOZILLA PUBLIC LICENSE
+                                Version 1.1
+
+                              ---------------
+
+1. Definitions.
+
+     1.0.1. "Commercial Use" means distribution or otherwise making the
+     Covered Code available to a third party.
+
+     1.1. "Contributor" means each entity that creates or contributes to
+     the creation of Modifications.
+
+     1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the Modifications
+     made by that particular Contributor.
+
+     1.3. "Covered Code" means the Original Code or Modifications or the
+     combination of the Original Code and Modifications, in each case
+     including portions thereof.
+
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+     1.5. "Executable" means Covered Code in any form other than Source
+     Code.
+
+     1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required by Exhibit
+     A.
+
+     1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this License.
+
+     1.8. "License" means this document.
+
+     1.8.1. "Licensable" means having the right to grant, to the maximum
+     extent possible, whether at the time of the initial grant or
+     subsequently acquired, any and all of the rights conveyed herein.
+
+     1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any previous
+     Modifications. When Covered Code is released as a series of files, a
+     Modification is:
+          A. Any addition to or deletion from the contents of a file
+          containing Original Code or previous Modifications.
+
+          B. Any new file that contains any part of the Original Code or
+          previous Modifications.
+
+     1.10. "Original Code" means Source Code of computer software code
+     which is described in the Source Code notice required by Exhibit A as
+     Original Code, and which, at the time of its release under this
+     License is not already Covered Code governed by this License.
+
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or
+     hereafter acquired, including without limitation,  method, process,
+     and apparatus claims, in any patent Licensable by grantor.
+
+     1.11. "Source Code" means the preferred form of the Covered Code for
+     making modifications to it, including all modules it contains, plus
+     any associated interface definition files, scripts used to control
+     compilation and installation of an Executable, or source code
+     differential comparisons against either the Original Code or another
+     well known, available Covered Code of the Contributor's choice. The
+     Source Code can be in a compressed or archival form, provided the
+     appropriate decompression or de-archiving software is widely available
+     for no charge.
+
+     1.12. "You" (or "Your")  means an individual or a legal entity
+     exercising rights under, and complying with all of the terms of, this
+     License or a future version of this License issued under Section 6.1.
+     For legal entities, "You" includes any entity which controls, is
+     controlled by, or is under common control with You. For purposes of
+     this definition, "control" means (a) the power, direct or indirect,
+     to cause the direction or management of such entity, whether by
+     contract or otherwise, or (b) ownership of more than fifty percent
+     (50%) of the outstanding shares or beneficial ownership of such
+     entity.
+
+2. Source Code License.
+
+     2.1. The Initial Developer Grant.
+     The Initial Developer hereby grants You a world-wide, royalty-free,
+     non-exclusive license, subject to third party intellectual property
+     claims:
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Initial Developer to use, reproduce,
+          modify, display, perform, sublicense and distribute the Original
+          Code (or portions thereof) with or without Modifications, and/or
+          as part of a Larger Work; and
+
+          (b) under Patents Claims infringed by the making, using or
+          selling of Original Code, to make, have made, use, practice,
+          sell, and offer for sale, and/or otherwise dispose of the
+          Original Code (or portions thereof).
+
+          (c) the licenses granted in this Section 2.1(a) and (b) are
+          effective on the date Initial Developer first distributes
+          Original Code under the terms of this License.
+
+          (d) Notwithstanding Section 2.1(b) above, no patent license is
+          granted: 1) for code that You delete from the Original Code; 2)
+          separate from the Original Code;  or 3) for infringements caused
+          by: i) the modification of the Original Code or ii) the
+          combination of the Original Code with other software or devices.
+
+     2.2. Contributor Grant.
+     Subject to third party intellectual property claims, each Contributor
+     hereby grants You a world-wide, royalty-free, non-exclusive license
+
+          (a)  under intellectual property rights (other than patent or
+          trademark) Licensable by Contributor, to use, reproduce, modify,
+          display, perform, sublicense and distribute the Modifications
+          created by such Contributor (or portions thereof) either on an
+          unmodified basis, with other Modifications, as Covered Code
+          and/or as part of a Larger Work; and
+
+          (b) under Patent Claims infringed by the making, using, or
+          selling of  Modifications made by that Contributor either alone
+          and/or in combination with its Contributor Version (or portions
+          of such combination), to make, use, sell, offer for sale, have
+          made, and/or otherwise dispose of: 1) Modifications made by that
+          Contributor (or portions thereof); and 2) the combination of
+          Modifications made by that Contributor with its Contributor
+          Version (or portions of such combination).
+
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+          effective on the date Contributor first makes Commercial Use of
+          the Covered Code.
+
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is
+          granted: 1) for any code that Contributor has deleted from the
+          Contributor Version; 2)  separate from the Contributor Version;
+          3)  for infringements caused by: i) third party modifications of
+          Contributor Version or ii)  the combination of Modifications made
+          by that Contributor with other software  (except as part of the
+          Contributor Version) or other devices; or 4) under Patent Claims
+          infringed by Covered Code in the absence of Modifications made by
+          that Contributor.
+
+3. Distribution Obligations.
+
+     3.1. Application of License.
+     The Modifications which You create or to which You contribute are
+     governed by the terms of this License, including without limitation
+     Section 2.2. The Source Code version of Covered Code may be
+     distributed only under the terms of this License or a future version
+     of this License released under Section 6.1, and You must include a
+     copy of this License with every copy of the Source Code You
+     distribute. You may not offer or impose any terms on any Source Code
+     version that alters or restricts the applicable version of this
+     License or the recipients' rights hereunder. However, You may include
+     an additional document offering the additional rights described in
+     Section 3.5.
+
+     3.2. Availability of Source Code.
+     Any Modification which You create or to which You contribute must be
+     made available in Source Code form under the terms of this License
+     either on the same media as an Executable version or via an accepted
+     Electronic Distribution Mechanism to anyone to whom you made an
+     Executable version available; and if made available via Electronic
+     Distribution Mechanism, must remain available for at least twelve (12)
+     months after the date it initially became available, or at least six
+     (6) months after a subsequent version of that particular Modification
+     has been made available to such recipients. You are responsible for
+     ensuring that the Source Code version remains available even if the
+     Electronic Distribution Mechanism is maintained by a third party.
+
+     3.3. Description of Modifications.
+     You must cause all Covered Code to which You contribute to contain a
+     file documenting the changes You made to create that Covered Code and
+     the date of any change. You must include a prominent statement that
+     the Modification is derived, directly or indirectly, from Original
+     Code provided by the Initial Developer and including the name of the
+     Initial Developer in (a) the Source Code, and (b) in any notice in an
+     Executable version or related documentation in which You describe the
+     origin or ownership of the Covered Code.
+
+     3.4. Intellectual Property Matters
+          (a) Third Party Claims.
+          If Contributor has knowledge that a license under a third party's
+          intellectual property rights is required to exercise the rights
+          granted by such Contributor under Sections 2.1 or 2.2,
+          Contributor must include a text file with the Source Code
+          distribution titled "LEGAL" which describes the claim and the
+          party making the claim in sufficient detail that a recipient will
+          know whom to contact. If Contributor obtains such knowledge after
+          the Modification is made available as described in Section 3.2,
+          Contributor shall promptly modify the LEGAL file in all copies
+          Contributor makes available thereafter and shall take other steps
+          (such as notifying appropriate mailing lists or newsgroups)
+          reasonably calculated to inform those who received the Covered
+          Code that new knowledge has been obtained.
+
+          (b) Contributor APIs.
+          If Contributor's Modifications include an application programming
+          interface and Contributor has knowledge of patent licenses which
+          are reasonably necessary to implement that API, Contributor must
+          also include this information in the LEGAL file.
+
+               (c)    Representations.
+          Contributor represents that, except as disclosed pursuant to
+          Section 3.4(a) above, Contributor believes that Contributor's
+          Modifications are Contributor's original creation(s) and/or
+          Contributor has sufficient rights to grant the rights conveyed by
+          this License.
+
+     3.5. Required Notices.
+     You must duplicate the notice in Exhibit A in each file of the Source
+     Code.  If it is not possible to put such notice in a particular Source
+     Code file due to its structure, then You must include such notice in a
+     location (such as a relevant directory) where a user would be likely
+     to look for such a notice.  If You created one or more Modification(s)
+     You may add your name as a Contributor to the notice described in
+     Exhibit A.  You must also duplicate this License in any documentation
+     for the Source Code where You describe recipients' rights or ownership
+     rights relating to Covered Code.  You may choose to offer, and to
+     charge a fee for, warranty, support, indemnity or liability
+     obligations to one or more recipients of Covered Code. However, You
+     may do so only on Your own behalf, and not on behalf of the Initial
+     Developer or any Contributor. You must make it absolutely clear than
+     any such warranty, support, indemnity or liability obligation is
+     offered by You alone, and You hereby agree to indemnify the Initial
+     Developer and every Contributor for any liability incurred by the
+     Initial Developer or such Contributor as a result of warranty,
+     support, indemnity or liability terms You offer.
+
+     3.6. Distribution of Executable Versions.
+     You may distribute Covered Code in Executable form only if the
+     requirements of Section 3.1-3.5 have been met for that Covered Code,
+     and if You include a notice stating that the Source Code version of
+     the Covered Code is available under the terms of this License,
+     including a description of how and where You have fulfilled the
+     obligations of Section 3.2. The notice must be conspicuously included
+     in any notice in an Executable version, related documentation or
+     collateral in which You describe recipients' rights relating to the
+     Covered Code. You may distribute the Executable version of Covered
+     Code or ownership rights under a license of Your choice, which may
+     contain terms different from this License, provided that You are in
+     compliance with the terms of this License and that the license for the
+     Executable version does not attempt to limit or alter the recipient's
+     rights in the Source Code version from the rights set forth in this
+     License. If You distribute the Executable version under a different
+     license You must make it absolutely clear that any terms which differ
+     from this License are offered by You alone, not by the Initial
+     Developer or any Contributor. You hereby agree to indemnify the
+     Initial Developer and every Contributor for any liability incurred by
+     the Initial Developer or such Contributor as a result of any such
+     terms You offer.
+
+     3.7. Larger Works.
+     You may create a Larger Work by combining Covered Code with other code
+     not governed by the terms of this License and distribute the Larger
+     Work as a single product. In such a case, You must make sure the
+     requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+     If it is impossible for You to comply with any of the terms of this
+     License with respect to some or all of the Covered Code due to
+     statute, judicial order, or regulation then You must: (a) comply with
+     the terms of this License to the maximum extent possible; and (b)
+     describe the limitations and the code they affect. Such description
+     must be included in the LEGAL file described in Section 3.4 and must
+     be included with all distributions of the Source Code. Except to the
+     extent prohibited by statute or regulation, such description must be
+     sufficiently detailed for a recipient of ordinary skill to be able to
+     understand it.
+
+5. Application of this License.
+
+     This License applies to code to which the Initial Developer has
+     attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+     6.1. New Versions.
+     Netscape Communications Corporation ("Netscape") may publish revised
+     and/or new versions of the License from time to time. Each version
+     will be given a distinguishing version number.
+
+     6.2. Effect of New Versions.
+     Once Covered Code has been published under a particular version of the
+     License, You may always continue to use it under the terms of that
+     version. You may also choose to use such Covered Code under the terms
+     of any subsequent version of the License published by Netscape. No one
+     other than Netscape has the right to modify the terms applicable to
+     Covered Code created under this License.
+
+     6.3. Derivative Works.
+     If You create or use a modified version of this License (which you may
+     only do in order to apply it to code which is not already Covered Code
+     governed by this License), You must (a) rename Your license so that
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your
+     license (except to note that your license differs from this License)
+     and (b) otherwise make it clear that Your version of the license
+     contains terms which differ from the Mozilla Public License and
+     Netscape Public License. (Filling in the name of the Initial
+     Developer, Original Code or Contributor in the notice described in
+     Exhibit A shall not of themselves be deemed to be modifications of
+     this License.)
+
+7. DISCLAIMER OF WARRANTY.
+
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+8. TERMINATION.
+
+     8.1.  This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and fail to cure
+     such breach within 30 days of becoming aware of the breach. All
+     sublicenses to the Covered Code which are properly granted shall
+     survive any termination of this License. Provisions which, by their
+     nature, must remain in effect beyond the termination of this License
+     shall survive.
+
+     8.2.  If You initiate litigation by asserting a patent infringement
+     claim (excluding declatory judgment actions) against Initial Developer
+     or a Contributor (the Initial Developer or Contributor against whom
+     You file such action is referred to as "Participant")  alleging that:
+
+     (a)  such Participant's Contributor Version directly or indirectly
+     infringes any patent, then any and all rights granted by such
+     Participant to You under Sections 2.1 and/or 2.2 of this License
+     shall, upon 60 days notice from Participant terminate prospectively,
+     unless if within 60 days after receipt of notice You either: (i)
+     agree in writing to pay Participant a mutually agreeable reasonable
+     royalty for Your past and future use of Modifications made by such
+     Participant, or (ii) withdraw Your litigation claim with respect to
+     the Contributor Version against such Participant.  If within 60 days
+     of notice, a reasonable royalty and payment arrangement are not
+     mutually agreed upon in writing by the parties or the litigation claim
+     is not withdrawn, the rights granted by Participant to You under
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+     the 60 day notice period specified above.
+
+     (b)  any software, hardware, or device, other than such Participant's
+     Contributor Version, directly or indirectly infringes any patent, then
+     any rights granted to You by such Participant under Sections 2.1(b)
+     and 2.2(b) are revoked effective as of the date You first made, used,
+     sold, distributed, or had made, Modifications made by that
+     Participant.
+
+     8.3.  If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly or
+     indirectly infringes any patent where such claim is resolved (such as
+     by license or settlement) prior to the initiation of patent
+     infringement litigation, then the reasonable value of the licenses
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken
+     into account in determining the amount or value of any payment or
+     license.
+
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and resellers)
+     which have been validly granted by You or any distributor hereunder
+     prior to termination shall survive termination.
+
+9. LIMITATION OF LIABILITY.
+
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+10. U.S. GOVERNMENT END USERS.
+
+     The Covered Code is a "commercial item," as that term is defined in
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+     software" and "commercial computer software documentation," as such
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+     all U.S. Government End Users acquire Covered Code with only those
+     rights set forth herein.
+
+11. MISCELLANEOUS.
+
+     This License represents the complete agreement concerning subject
+     matter hereof. If any provision of this License is held to be
+     unenforceable, such provision shall be reformed only to the extent
+     necessary to make it enforceable. This License shall be governed by
+     California law provisions (except to the extent applicable law, if
+     any, provides otherwise), excluding its conflict-of-law provisions.
+     With respect to disputes in which at least one party is a citizen of,
+     or an entity chartered or registered to do business in the United
+     States of America, any litigation relating to this License shall be
+     subject to the jurisdiction of the Federal Courts of the Northern
+     District of California, with venue lying in Santa Clara County,
+     California, with the losing party responsible for costs, including
+     without limitation, court costs and reasonable attorneys' fees and
+     expenses. The application of the United Nations Convention on
+     Contracts for the International Sale of Goods is expressly excluded.
+     Any law or regulation which provides that the language of a contract
+     shall be construed against the drafter shall not apply to this
+     License.
+
+12. RESPONSIBILITY FOR CLAIMS.
+
+     As between Initial Developer and the Contributors, each party is
+     responsible for claims and damages arising, directly or indirectly,
+     out of its utilization of rights under this License and You agree to
+     work with Initial Developer and Contributors to distribute such
+     responsibility on an equitable basis. Nothing herein is intended or
+     shall be deemed to constitute any admission of liability.
+
+13. MULTIPLE-LICENSED CODE.
+
+     Initial Developer may designate portions of the Covered Code as
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
+     Developer permits you to utilize portions of the Covered Code under
+     Your choice of the NPL or the alternative licenses, if any, specified
+     by the Initial Developer in the file described in Exhibit A.
+
+EXHIBIT A -Mozilla Public License.
+
+     ``The contents of this file are subject to the Mozilla Public License
+     Version 1.1 (the "License"); you may not use this file except in
+     compliance with the License. You may obtain a copy of the License at
+     http://www.mozilla.org/MPL/
+
+     Software distributed under the License is distributed on an "AS IS"
+     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+     License for the specific language governing rights and limitations
+     under the License.
+
+     The Original Code is ______________________________________.
+
+     The Initial Developer of the Original Code is ________________________.
+     Portions created by ______________________ are Copyright (C) ______
+     _______________________. All Rights Reserved.
+
+     Contributor(s): ______________________________________.
+
+     Alternatively, the contents of this file may be used under the terms
+     of the _____ license (the  "[___] License"), in which case the
+     provisions of [______] License are applicable instead of those
+     above.  If you wish to allow use of your version of this file only
+     under the terms of the [____] License and not to allow others to use
+     your version of this file under the MPL, indicate your decision by
+     deleting  the provisions above and replace  them with the notice and
+     other provisions required by the [___] License.  If you do not delete
+     the provisions above, a recipient may use your version of this file
+     under either the MPL or the [___] License."
+
+     [NOTE: The text of this Exhibit A may differ slightly from the text of
+     the notices in the Source Code files of the Original Code. You should
+     use the text of this Exhibit A rather than the text found in the
+     Original Code Source Code for Your Modifications.]
diff --git a/lib/LICENSE-PublicDomain b/lib/LICENSE-PublicDomain
new file mode 100644
index 0000000..8a71ce0
--- /dev/null
+++ b/lib/LICENSE-PublicDomain
@@ -0,0 +1 @@
+This software has been placed in the public domain by its author(s).
diff --git a/lib/LICENSE-antlr b/lib/LICENSE-antlr
new file mode 100644
index 0000000..6041290
--- /dev/null
+++ b/lib/LICENSE-antlr
@@ -0,0 +1,29 @@
+Copyright (c) 2003-2008, Terence Parr
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of the author nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-args4j b/lib/LICENSE-args4j
new file mode 100644
index 0000000..36cd75f
--- /dev/null
+++ b/lib/LICENSE-args4j
@@ -0,0 +1,32 @@
+Copyright (c) 2003, Kohsuke Kawaguchi
+All rights reserved.
+
+Redistribution and use in source and binary forms,
+with or without modification, are permitted provided
+that the following conditions are met:
+
+    * Redistributions of source code must retain
+      the above copyright notice, this list of
+      conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce
+      the above copyright notice, this list of
+      conditions and the following disclaimer in
+      the documentation and/or other materials
+      provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT
+HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-automaton b/lib/LICENSE-automaton
new file mode 100644
index 0000000..72dcb1c
--- /dev/null
+++ b/lib/LICENSE-automaton
@@ -0,0 +1,28 @@
+Copyright (c) 2007-2009, dk.brics.automaton
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group nor the names of its
+      contributors may be used to endorse or promote products derived from
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-bouncycastle b/lib/LICENSE-bouncycastle
new file mode 100644
index 0000000..d17a4bc
--- /dev/null
+++ b/lib/LICENSE-bouncycastle
@@ -0,0 +1,21 @@
+Copyright (c) 2000 - 2012 The Legion Of The Bouncy Castle
+(http://www.bouncycastle.org)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/LICENSE-clippy b/lib/LICENSE-clippy
new file mode 100644
index 0000000..b0feeae
--- /dev/null
+++ b/lib/LICENSE-clippy
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Tom Preston-Werner
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/LICENSE-codemirror b/lib/LICENSE-codemirror
new file mode 100644
index 0000000..7df9fec
--- /dev/null
+++ b/lib/LICENSE-codemirror
@@ -0,0 +1,44 @@
+Copyright (C) 2013 by Marijn Haverbeke <marijnh@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+----
+
+codemirror Python mode:
+----
+The MIT License
+
+Copyright (c) 2010 Timothy Farrell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/LICENSE-diffy b/lib/LICENSE-diffy
new file mode 100644
index 0000000..2b58536
--- /dev/null
+++ b/lib/LICENSE-diffy
@@ -0,0 +1 @@
+link:http://creativecommons.org/licenses/by/3.0/us/[CC-BY 3.0] (c) Sara Owens, inkylabs.com
diff --git a/lib/LICENSE-h2 b/lib/LICENSE-h2
new file mode 100644
index 0000000..1be4fba8
--- /dev/null
+++ b/lib/LICENSE-h2
@@ -0,0 +1,710 @@
+H2 is dual licensed and available under a modified version of the
+MPL 1.1 (Mozilla Public License) or under the (unmodified) EPL 1.0.
+----
+
+link:http://www.h2database.com/html/license.html[H2 License]
+
+----
+H2 License - Version 1.0
+1. Definitions
+
+1.0.1. "Commercial Use" means distribution or otherwise making the
+       Covered Code available to a third party.
+
+1.1. "Contributor" means each entity that creates or contributes
+     to the creation of Modifications.
+
+1.2. "Contributor Version" means the combination of the Original
+     Code, prior Modifications used by a Contributor, and the
+     Modifications made by that particular Contributor.
+
+1.3. "Covered Code" means the Original Code or Modifications or
+     the combination of the Original Code and Modifications, in each
+     case including portions thereof.
+
+1.4. "Electronic Distribution Mechanism" means a mechanism generally
+     accepted in the software development community for the electronic
+     transfer of data.
+
+1.5. "Executable" means Covered Code in any form other than Source Code.
+
+1.6. "Initial Developer" means the individual or entity identified
+     as the Initial Developer in the Source Code notice required
+     by Exhibit A.
+
+1.7. "Larger Work" means a work which combines Covered Code or
+     portions thereof with code not governed by the terms of this
+     License.
+
+1.8. "License" means this document.
+
+1.8.1. "Licensable" means having the right to grant, to the maximum
+       extent possible, whether at the time of the initial grant
+       or subsequently acquired, any and all of the rights conveyed
+       herein.
+
+1.9. "Modifications" means any addition to or deletion from the
+     substance or structure of either the Original Code or any
+     previous Modifications. When Covered Code is released as a
+     series of files, a Modification is:
+
+1.9.a. Any addition to or deletion from the contents of a file
+       containing Original Code or previous Modifications.
+
+1.9.b. Any new file that contains any part of the Original Code or
+       previous Modifications.
+
+1.10. "Original Code" means Source Code of computer software
+      code which is described in the Source Code notice required
+      by Exhibit A as Original Code, and which, at the time of
+      its release under this License is not already Covered Code
+      governed by this License.
+
+1.10.1. "Patent Claims" means any patent claim(s), now owned or
+        hereafter acquired, including without limitation, method,
+        process, and apparatus claims, in any patent Licensable
+        by grantor.
+
+1.11. "Source Code" means the preferred form of the Covered Code
+      for making modifications to it, including all modules it
+      contains, plus any associated interface definition files,
+      scripts used to control compilation and installation of an
+      Executable, or source code differential comparisons against
+      either the Original Code or another well known, available
+      Covered Code of the Contributor's choice. The Source Code can
+      be in a compressed or archival form, provided the appropriate
+      decompression or de-archiving software is widely available
+      for no charge.
+
+1.12. "You" (or "Your") means an individual or a legal entity
+      exercising rights under, and complying with all of the terms
+      of, this License or a future version of this License issued
+      under Section 6.1. For legal entities, "You" includes any
+      entity which controls, is controlled by, or is under common
+      control with You. For purposes of this definition, "control"
+      means (a) the power, direct or indirect, to cause the direction
+      or management of such entity, whether by contract or otherwise,
+      or (b) ownership of more than fifty percent (50%) of the
+      outstanding shares or beneficial ownership of such entity.
+
+2. Source Code License
+
+2.1. The Initial Developer Grant
+
+The Initial Developer hereby grants You a world-wide, royalty-free,
+non-exclusive license, subject to third party intellectual property
+claims:
+
+2.1.a. under intellectual property rights (other than patent
+       or trademark) Licensable by Initial Developer to use,
+       reproduce, modify, display, perform, sublicense and distribute
+       the Original Code (or portions thereof) with or without
+       Modifications, and/or as part of a Larger Work; and
+
+2.1.b. under Patents Claims infringed by the making, using or selling
+       of Original Code, to make, have made, use, practice, sell,
+       and offer for sale, and/or otherwise dispose of the Original
+       Code (or portions thereof).
+
+2.1.c. the licenses granted in this Section 2.1 (a) and (b) are
+       effective on the date Initial Developer first distributes
+       Original Code under the terms of this License.
+
+2.1.d. Notwithstanding Section 2.1 (b) above, no patent license is
+       granted: 1) for code that You delete from the Original Code;
+       2) separate from the Original Code; or 3) for infringements
+       caused by: i) the modification of the Original Code or ii)
+       the combination of the Original Code with other software
+       or devices.
+
+2.2. Contributor Grant
+
+Subject to third party intellectual property claims, each Contributor
+hereby grants You a world-wide, royalty-free, non-exclusive license
+
+2.2.a. under intellectual property rights (other than patent or
+       trademark) Licensable by Contributor, to use, reproduce,
+       modify, display, perform, sublicense and distribute the
+       Modifications created by such Contributor (or portions
+       thereof) either on an unmodified basis, with other
+       Modifications, as Covered Code and/or as part of a Larger
+       Work; and
+
+2.2.b. under Patent Claims infringed by the making, using, or selling
+       of Modifications made by that Contributor either alone and/or
+       in combination with its Contributor Version (or portions
+       of such combination), to make, use, sell, offer for sale,
+       have made, and/or otherwise dispose of: 1) Modifications
+       made by that Contributor (or portions thereof); and 2) the
+       combination of Modifications made by that Contributor with
+       its Contributor Version (or portions of such combination).
+
+2.2.c. the licenses granted in Sections 2.2 (a) and 2.2 (b) are
+       effective on the date Contributor first makes Commercial
+       Use of the Covered Code.
+
+2.2.c. Notwithstanding Section 2.2 (b) above, no patent license is
+       granted: 1) for any code that Contributor has deleted from
+       the Contributor Version; 2) separate from the Contributor
+       Version; 3) for infringements caused by: i) third party
+       modifications of Contributor Version or ii) the combination
+       of Modifications made by that Contributor with other software
+       (except as part of the Contributor Version) or other devices;
+       or 4) under Patent Claims infringed by Covered Code in the
+       absence of Modifications made by that Contributor.
+
+3. Distribution Obligations
+
+3.1. Application of License
+
+The Modifications which You create or to which You contribute
+are governed by the terms of this License, including without
+limitation Section 2.2. The Source Code version of Covered Code may
+be distributed only under the terms of this License or a future
+version of this License released under Section 6.1, and You must
+include a copy of this License with every copy of the Source Code
+You distribute. You may not offer or impose any terms on any Source
+Code version that alters or restricts the applicable version of
+this License or the recipients' rights hereunder. However, You
+may include an additional document offering the additional rights
+described in Section 3.5.
+
+3.2. Availability of Source Code
+
+Any Modification which You create or to which You contribute must
+be made available in Source Code form under the terms of this
+License either on the same media as an Executable version or via
+an accepted Electronic Distribution Mechanism to anyone to whom
+you made an Executable version available; and if made available
+via Electronic Distribution Mechanism, must remain available for
+at least twelve (12) months after the date it initially became
+available, or at least six (6) months after a subsequent version
+of that particular Modification has been made available to such
+recipients. You are responsible for ensuring that the Source Code
+version remains available even if the Electronic Distribution
+Mechanism is maintained by a third party.
+
+3.3. Description of Modifications
+
+You must cause all Covered Code to which You contribute to contain
+a file documenting the changes You made to create that Covered
+Code and the date of any change. You must include a prominent
+statement that the Modification is derived, directly or indirectly,
+from Original Code provided by the Initial Developer and including
+the name of the Initial Developer in (a) the Source Code, and (b)
+in any notice in an Executable version or related documentation in
+which You describe the origin or ownership of the Covered Code.
+
+3.4. Intellectual Property Matters
+
+3.4.a. Third Party Claims: If Contributor has knowledge that
+       a license under a third party's intellectual property
+       rights is required to exercise the rights granted by such
+       Contributor under Sections 2.1 or 2.2, Contributor must
+       include a text file with the Source Code distribution titled
+       "LEGAL" which describes the claim and the party making the
+       claim in sufficient detail that a recipient will know whom
+       to contact. If Contributor obtains such knowledge after the
+       Modification is made available as described in Section 3.2,
+       Contributor shall promptly modify the LEGAL file in all
+       copies Contributor makes available thereafter and shall take
+       other steps (such as notifying appropriate mailing lists or
+       newsgroups) reasonably calculated to inform those who received
+       the Covered Code that new knowledge has been obtained.
+
+3.4.b. Contributor APIs: If Contributor's Modifications include
+       an application programming interface and Contributor has
+       knowledge of patent licenses which are reasonably necessary
+       to implement that API, Contributor must also include this
+       information in the legal file.
+
+3.4.c. Representations: Contributor represents that, except as
+       disclosed pursuant to Section 3.4 (a) above, Contributor
+       believes that Contributor's Modifications are Contributor's
+       original creation(s) and/or Contributor has sufficient rights
+       to grant the rights conveyed by this License.
+
+3.5. Required Notices
+
+You must duplicate the notice in Exhibit A in each file of
+the Source Code. If it is not possible to put such notice in a
+particular Source Code file due to its structure, then You must
+include such notice in a location (such as a relevant directory)
+where a user would be likely to look for such a notice. If You
+created one or more Modification(s) You may add your name as a
+Contributor to the notice described in Exhibit A. You must also
+duplicate this License in any documentation for the Source Code
+where You describe recipients' rights or ownership rights relating
+to Covered Code. You may choose to offer, and to charge a fee for,
+warranty, support, indemnity or liability obligations to one or
+more recipients of Covered Code. However, You may do so only on
+Your own behalf, and not on behalf of the Initial Developer or
+any Contributor. You must make it absolutely clear than any such
+warranty, support, indemnity or liability obligation is offered by
+You alone, and You hereby agree to indemnify the Initial Developer
+and every Contributor for any liability incurred by the Initial
+Developer or such Contributor as a result of warranty, support,
+indemnity or liability terms You offer.
+
+3.6. Distribution of Executable Versions
+
+You may distribute Covered Code in Executable form only if the
+requirements of Sections 3.1, 3.2, 3.3, 3.4 and 3.5 have been met
+for that Covered Code, and if You include a notice stating that
+the Source Code version of the Covered Code is available under the
+terms of this License, including a description of how and where
+You have fulfilled the obligations of Section 3.2. The notice
+must be conspicuously included in any notice in an Executable
+version, related documentation or collateral in which You describe
+recipients' rights relating to the Covered Code. You may distribute
+the Executable version of Covered Code or ownership rights under
+a license of Your choice, which may contain terms different from
+this License, provided that You are in compliance with the terms
+of this License and that the license for the Executable version
+does not attempt to limit or alter the recipient's rights in the
+Source Code version from the rights set forth in this License. If
+You distribute the Executable version under a different license You
+must make it absolutely clear that any terms which differ from this
+License are offered by You alone, not by the Initial Developer or any
+Contributor. You hereby agree to indemnify the Initial Developer and
+every Contributor for any liability incurred by the Initial Developer
+or such Contributor as a result of any such terms You offer.
+
+3.7. Larger Works
+
+You may create a Larger Work by combining Covered Code with other
+code not governed by the terms of this License and distribute the
+Larger Work as a single product. In such a case, You must make sure
+the requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+If it is impossible for You to comply with any of the terms of
+this License with respect to some or all of the Covered Code due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description
+must be included in the legal file described in Section 3.4 and
+must be included with all distributions of the Source Code. Except
+to the extent prohibited by statute or regulation, such description
+must be sufficiently detailed for a recipient of ordinary skill to
+be able to understand it.
+
+5. Application of this License.
+
+This License applies to code to which the Initial Developer has
+attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+6.1. New Versions
+
+The H2 Group may publish revised and/or new versions of the License
+from time to time. Each version will be given a distinguishing
+version number.
+
+6.2. Effect of New Versions
+
+Once Covered Code has been published under a particular version of
+the License, You may always continue to use it under the terms of
+that version. You may also choose to use such Covered Code under the
+terms of any subsequent version of the License published by the H2
+Group. No one other than the H2 Group has the right to modify the
+terms applicable to Covered Code created under this License.
+
+6.3. Derivative Works
+
+If You create or use a modified version of this License (which you
+may only do in order to apply it to code which is not already Covered
+Code governed by this License), You must (a) rename Your license so
+that the phrases "H2 Group", "H2" or any confusingly similar phrase
+do not appear in your license (except to note that your license
+differs from this License) and (b) otherwise make it clear that
+Your version of the license contains terms which differ from the
+H2 License. (Filling in the name of the Initial Developer, Original
+Code or Contributor in the notice described in Exhibit A shall not
+of themselves be deemed to be modifications of this License.)
+
+7. Disclaimer of Warranty
+
+Covered code is provided under this license on an "as is" basis,
+without warranty of any kind, either expressed or implied,
+including, without limitation, warranties that the covered code
+is free of defects, merchantable, fit for a particular purpose or
+non-infringing. The entire risk as to the quality and performance
+of the covered code is with you. Should any covered code prove
+defective in any respect, you (not the initial developer or any
+other contributor) assume the cost of any necessary servicing,
+repair or correction. This disclaimer of warranty constitutes
+an essential part of this license. No use of any covered code is
+authorized hereunder except under this disclaimer.
+
+8. Termination
+
+8.1. This License and the rights granted hereunder will terminate
+     automatically if You fail to comply with terms herein and
+     fail to cure such breach within 30 days of becoming aware
+     of the breach. All sublicenses to the Covered Code which
+     are properly granted shall survive any termination of this
+     License. Provisions which, by their nature, must remain in
+     effect beyond the termination of this License shall survive.
+
+8.2. If You initiate litigation by asserting a patent infringement
+     claim (excluding declaratory judgment actions) against
+     Initial Developer or a Contributor (the Initial Developer or
+     Contributor against whom You file such action is referred to as
+     "Participant") alleging that:
+
+8.2.a. such Participant's Contributor Version directly or indirectly
+       infringes any patent, then any and all rights granted by
+       such Participant to You under Sections 2.1 and/or 2.2 of this
+       License shall, upon 60 days notice from Participant terminate
+       prospectively, unless if within 60 days after receipt of
+       notice You either: (i) agree in writing to pay Participant
+       a mutually agreeable reasonable royalty for Your past and
+       future use of Modifications made by such Participant, or (ii)
+       withdraw Your litigation claim with respect to the Contributor
+       Version against such Participant. If within 60 days of notice,
+       a reasonable royalty and payment arrangement are not mutually
+       agreed upon in writing by the parties or the litigation claim
+       is not withdrawn, the rights granted by Participant to You
+       under Sections 2.1 and/or 2.2 automatically terminate at
+       the expiration of the 60 day notice period specified above.
+
+8.2.b. any software, hardware, or device, other than such
+       Participant's Contributor Version, directly or indirectly
+       infringes any patent, then any rights granted to You by
+       such Participant under Sections 2.1(b) and 2.2(b) are
+       revoked effective as of the date You first made, used,
+       sold, distributed, or had made, Modifications made by that
+       Participant.
+
+8.3. If You assert a patent infringement claim against Participant
+     alleging that such Participant's Contributor Version directly
+     or indirectly infringes any patent where such claim is resolved
+     (such as by license or settlement) prior to the initiation of
+     patent infringement litigation, then the reasonable value of
+     the licenses granted by such Participant under Sections 2.1
+     or 2.2 shall be taken into account in determining the amount
+     or value of any payment or license.
+
+8.4. In the event of termination under Sections 8.1 or 8.2 above,
+     all end user license agreements (excluding distributors and
+     resellers) which have been validly granted by You or any
+     distributor hereunder prior to termination shall survive
+     termination.
+
+9. Limitation of Liability
+
+Under no circumstances and under no legal theory, whether tort
+(including negligence), contract, or otherwise, shall you, the
+initial developer, any other contributor, or any distributor of
+covered code, or any supplier of any of such parties, be liable to
+any person for any indirect, special, incidental, or consequential
+damages of any character including, without limitation, damages for
+loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses, even if such party
+shall have been informed of the possibility of such damages. This
+limitation of liability shall not apply to liability for death or
+personal injury resulting from such party's negligence to the extent
+applicable law prohibits such limitation. Some jurisdictions do not
+allow the exclusion or limitation of incidental or consequential
+damages, so this exclusion and limitation may not apply to you.
+
+10. United States Government End Users
+
+The Covered Code is a "commercial item", as that term is defined in
+48 C.F.R. 2.101 (October 1995), consisting of "commercial computer
+software" and "commercial computer software documentation", as such
+terms are used in 48 C.F.R. 12.212 (September 1995). Consistent
+with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4
+(June 1995), all U.S. Government End Users acquire Covered Code
+with only those rights set forth herein.
+
+11. Miscellaneous
+
+This License represents the complete agreement concerning subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. This License shall be governed
+by California law provisions (except to the extent applicable
+law, if any, provides otherwise), excluding its conflict-of-law
+provisions. With respect to disputes in which at least one party is
+a citizen of, or an entity chartered or registered to do business in
+United States of America, any litigation relating to this License
+shall be subject to the jurisdiction of the Federal Courts of the
+Northern District of California, with venue lying in Santa Clara
+County, California, with the losing party responsible for costs,
+including without limitation, court costs and reasonable attorneys'
+fees and expenses. The application of the United Nations Convention
+on Contracts for the International Sale of Goods is expressly
+excluded. Any law or regulation which provides that the language of
+a contract shall be construed against the drafter shall not apply
+to this License.
+
+12. Responsibility for Claims
+
+As between Initial Developer and the Contributors, each party is
+responsible for claims and damages arising, directly or indirectly,
+out of its utilization of rights under this License and You agree
+to work with Initial Developer and Contributors to distribute such
+responsibility on an equitable basis. Nothing herein is intended
+or shall be deemed to constitute any admission of liability.
+
+13. Multiple-Licensed Code
+
+Initial Developer may designate portions of the Covered Code as
+"Multiple-Licensed". "Multiple-Licensed" means that the Initial
+Developer permits you to utilize portions of the Covered Code under
+Your choice of this or the alternative licenses, if any, specified
+by the Initial Developer in the file described in Exhibit A.
+
+Exhibit A
+
+Multiple-Licensed under the H2 License, Version 1.0,
+and under the Eclipse Public License, Version 1.0
+(http://h2database.com/html/license.html).
+Initial Developer: H2 Group
+----
+
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+   documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free copyright
+   license to reproduce, prepare derivative works of, publicly display,
+   publicly perform, distribute and sublicense the Contribution of such
+   Contributor, if any, and such derivative works, in source code and
+   object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free patent
+   license under Licensed Patents to make, use, sell, offer to sell,
+   import and otherwise transfer the Contribution of such Contributor,
+   if any, in source code and object code form. This patent license
+   shall apply to the combination of the Contribution and the Program
+   if, at the time the Contribution is added by the Contributor, such
+   addition of the Contribution causes such combination to be covered
+   by the Licensed Patents. The patent license shall not apply to any
+   other combinations which include the Contribution. No hardware per
+   se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+   licenses to its Contributions set forth herein, no assurances are
+   provided by any Contributor that the Program does not infringe
+   the patent or other intellectual property rights of any other
+   entity. Each Contributor disclaims any liability to Recipient
+   for claims brought by any other entity based on infringement
+   of intellectual property rights or otherwise. As a condition to
+   exercising the rights and licenses granted hereunder, each Recipient
+   hereby assumes sole responsibility to secure any other intellectual
+   property rights needed, if any. For example, if a third party patent
+   license is required to allow Recipient to distribute the Program,
+   it is Recipient's responsibility to acquire that license before
+   distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+   sufficient copyright rights in its Contribution, if any, to grant
+   the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+  form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+   and conditions, express and implied, including warranties or
+   conditions of title and non-infringement, and implied warranties or
+   conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+    for damages, including direct, indirect, special, incidental and
+    consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+     are offered by that Contributor alone and not by any other
+     party; and
+
+iv) states that source code for the Program is available from such
+    Contributor, and informs licensees how to obtain it in a reasonable
+    manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
+----
+
+----
+Export Control Classification Number (ECCN)
+
+As far as we know, the U.S. Export Control Classification Number
+(ECCN) for this software is 5D002. However, for legal reasons, we
+can make no warranty that this information is correct. For details,
+see also the Apache Software Foundation Export Classifications page.
diff --git a/lib/LICENSE-jgit b/lib/LICENSE-jgit
new file mode 100644
index 0000000..1b85c64
--- /dev/null
+++ b/lib/LICENSE-jgit
@@ -0,0 +1,37 @@
+This program and the accompanying materials are made available
+under the terms of the Eclipse Distribution License v1.0 which
+accompanies this distribution, is reproduced below, and is
+available at http://www.eclipse.org/org/documents/edl-v10.php
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the following
+conditions are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the following
+  disclaimer in the documentation and/or other materials provided
+  with the distribution.
+
+- Neither the name of the Eclipse Foundation, Inc. nor the
+  names of its contributors may be used to endorse or promote
+  products derived from this software without specific prior
+  written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-jsch b/lib/LICENSE-jsch
new file mode 100644
index 0000000..2cb0ddd
--- /dev/null
+++ b/lib/LICENSE-jsch
@@ -0,0 +1,26 @@
+Copyright (c) 2002-2012 Atsuhiko Yamanaka, JCraft,Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in
+     the documentation and/or other materials provided with the distribution.
+
+  3. The names of the authors may not be used to endorse or promote products
+     derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-ow2 b/lib/LICENSE-ow2
new file mode 100644
index 0000000..c5aba7b
--- /dev/null
+++ b/lib/LICENSE-ow2
@@ -0,0 +1,29 @@
+Copyright (c) 2000-2011 INRIA, France Telecom
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holders nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-postgresql b/lib/LICENSE-postgresql
new file mode 100644
index 0000000..fd416d2
--- /dev/null
+++ b/lib/LICENSE-postgresql
@@ -0,0 +1,26 @@
+Copyright (c) 1997-2011, PostgreSQL Global Development Group
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+3. Neither the name of the PostgreSQL Global Development Group nor the names
+   of its contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-prologcafe b/lib/LICENSE-prologcafe
new file mode 100644
index 0000000..7183d37
--- /dev/null
+++ b/lib/LICENSE-prologcafe
@@ -0,0 +1,593 @@
+Prolog Cafe (A Prolog to Java Translator System)
+Copyright (C) 1997-2009 by Mutsunori Banbara and Naoyuki Tamura
+
+Prolog Cafe is free software; you can redistribute it and/or modify
+it under the terms of either:
+
+  * the GNU General Public License as published by the Free Software
+    Foundation; either version 2 of the License, or (at your option)
+    any later version, or
+
+  * the Eclipse Public License
+----
+
+In the context of Gerrit Code Review, Prolog Cafe is consumed under
+the <<prologcafe_EPL,EPL>>. Gerrit Code Review uses a fork derived
+from the 1.2.5 release and offers the corresponding source code at
+link:https://gerrit.googlesource.com/prolog-cafe[].
+
+----
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+----
+
+[[prologcafe_EPL]]
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+   documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free copyright
+   license to reproduce, prepare derivative works of, publicly display,
+   publicly perform, distribute and sublicense the Contribution of such
+   Contributor, if any, and such derivative works, in source code and
+   object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+   grants Recipient a non-exclusive, worldwide, royalty-free patent
+   license under Licensed Patents to make, use, sell, offer to sell,
+   import and otherwise transfer the Contribution of such Contributor,
+   if any, in source code and object code form. This patent license
+   shall apply to the combination of the Contribution and the Program
+   if, at the time the Contribution is added by the Contributor, such
+   addition of the Contribution causes such combination to be covered
+   by the Licensed Patents. The patent license shall not apply to any
+   other combinations which include the Contribution. No hardware per
+   se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+   licenses to its Contributions set forth herein, no assurances are
+   provided by any Contributor that the Program does not infringe
+   the patent or other intellectual property rights of any other
+   entity. Each Contributor disclaims any liability to Recipient
+   for claims brought by any other entity based on infringement
+   of intellectual property rights or otherwise. As a condition to
+   exercising the rights and licenses granted hereunder, each Recipient
+   hereby assumes sole responsibility to secure any other intellectual
+   property rights needed, if any. For example, if a third party patent
+   license is required to allow Recipient to distribute the Program,
+   it is Recipient's responsibility to acquire that license before
+   distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+   sufficient copyright rights in its Contribution, if any, to grant
+   the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+  form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+   and conditions, express and implied, including warranties or
+   conditions of title and non-infringement, and implied warranties or
+   conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+    for damages, including direct, indirect, special, incidental and
+    consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+     are offered by that Contributor alone and not by any other
+     party; and
+
+iv) states that source code for the Program is available from such
+    Contributor, and informs licensees how to obtain it in a reasonable
+    manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
diff --git a/lib/LICENSE-protobuf b/lib/LICENSE-protobuf
new file mode 100644
index 0000000..705db57
--- /dev/null
+++ b/lib/LICENSE-protobuf
@@ -0,0 +1,33 @@
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+    * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it.  This code is not
+standalone and requires a support library to be linked with it.  This
+support library is itself covered by the above license.
diff --git a/lib/LICENSE-slf4j b/lib/LICENSE-slf4j
new file mode 100644
index 0000000..f5ecafa
--- /dev/null
+++ b/lib/LICENSE-slf4j
@@ -0,0 +1,21 @@
+Copyright (c) 2004-2008 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free  of charge, to any person obtaining
+a  copy  of this  software  and  associated  documentation files  (the
+"Software"), to  deal in  the Software without  restriction, including
+without limitation  the rights to  use, copy, modify,  merge, publish,
+distribute,  sublicense, and/or sell  copies of  the Software,  and to
+permit persons to whom the Software  is furnished to do so, subject to
+the following conditions:
+
+The  above  copyright  notice  and  this permission  notice  shall  be
+included in all copies or substantial portions of the Software.
+
+THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
+EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
+MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/antlr/BUCK b/lib/antlr/BUCK
new file mode 100644
index 0000000..732b459
--- /dev/null
+++ b/lib/antlr/BUCK
@@ -0,0 +1,48 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '3.2'
+
+maven_jar(
+  name = 'java_runtime',
+  id = 'org.antlr:antlr-runtime:' + VERSION,
+  sha1 = '31c746001016c6226bd7356c9f87a6a084ce3715',
+  license = 'antlr',
+)
+
+java_binary(
+  name = 'antlr-tool',
+  main_class = 'org.antlr.Tool',
+  deps = [':tool'],
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'stringtemplate',
+  id = 'org.antlr:stringtemplate:' + VERSION,
+  sha1 = '6fe2e3bb57daebd1555494818909f9664376dd6c',
+  license = 'antlr',
+  attach_source = False,
+  visibility = [],
+)
+
+maven_jar(
+  name = 'tool',
+  id = 'org.antlr:antlr:' + VERSION,
+  sha1 = '6b0acabea7bb3da058200a77178057e47e25cb69',
+  license = 'antlr',
+  deps = [
+    ':java_runtime',
+    ':stringtemplate',
+    ':antlr27',
+  ],
+  visibility = [],
+)
+
+maven_jar(
+  name = 'antlr27',
+  id = 'antlr:antlr:2.7.7',
+  sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0',
+  license = 'antlr',
+  attach_source = False,
+  visibility = [],
+)
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
new file mode 100644
index 0000000..0b07b69
--- /dev/null
+++ b/lib/asciidoctor/BUCK
@@ -0,0 +1,58 @@
+include_defs('//lib/maven.defs')
+
+java_binary(
+  name = 'asciidoc',
+  main_class = 'AsciiDoctor',
+  deps = [':asciidoc_lib'],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'asciidoc_lib',
+  srcs = ['java/AsciiDoctor.java'],
+  deps = [
+    ':asciidoctor',
+    ':jruby',
+    '//lib:args4j',
+    '//lib:guava',
+  ],
+  visibility = ['//tools/eclipse:classpath'],
+)
+
+java_binary(
+  name = 'doc_indexer',
+  main_class = 'DocIndexer',
+  deps = [':doc_indexer_lib'],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'doc_indexer_lib',
+  srcs = ['java/DocIndexer.java'],
+  deps = [
+    ':asciidoc_lib',
+    '//lib:args4j',
+    '//lib:guava',
+    '//lib/lucene:analyzers-common',
+    '//lib/lucene:core',
+  ],
+  visibility = ['//tools/eclipse:classpath'],
+)
+
+maven_jar(
+  name = 'asciidoctor',
+  id = 'org.asciidoctor:asciidoctor-java-integration:0.1.4',
+  sha1 = '3596c7142fd30d7b65a0e64ba294f3d9d4bd538f',
+  license = 'Apache2.0',
+  visibility = [],
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'jruby',
+  id = 'org.jruby:jruby-complete:1.7.4',
+  sha1 = '74984d84846523bd7da49064679ed1ccf199e1db',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = [],
+  attach_source = False,
+)
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
new file mode 100644
index 0000000..0613ff4
--- /dev/null
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -0,0 +1,185 @@
+// 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.
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import com.google.common.io.ByteStreams;
+
+import org.asciidoctor.Asciidoctor;
+import org.asciidoctor.AttributesBuilder;
+import org.asciidoctor.Options;
+import org.asciidoctor.OptionsBuilder;
+import org.asciidoctor.SafeMode;
+import org.asciidoctor.internal.JRubyAsciidoctor;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+public class AsciiDoctor {
+
+  private static final String DOCTYPE = "article";
+  private static final String ERUBY = "erb";
+
+  @Option(name = "-b", usage = "set output format backend")
+  private String backend = "html5";
+
+  @Option(name = "-z", usage = "output zip file")
+  private String zipFile;
+
+  @Option(name = "--in-ext", usage = "extension for input files")
+  private String inExt = ".txt";
+
+  @Option(name = "--out-ext", usage = "extension for output files")
+  private String outExt = ".html";
+
+  @Option(name = "-a", usage =
+      "a list of attributes, in the form key or key=value pair")
+  private List<String> attributes = new ArrayList<String>();
+
+  @Argument(usage = "input files")
+  private List<String> inputFiles = new ArrayList<String>();
+
+  public static String mapInFileToOutFile(
+      String inFile, String inExt, String outExt) {
+    String basename = new File(inFile).getName();
+    if (basename.endsWith(inExt)) {
+      basename = basename.substring(0, basename.length() - inExt.length());
+    } else {
+      // Strip out the last extension
+      int pos = basename.lastIndexOf('.');
+      if (pos > 0) {
+        basename = basename.substring(0, pos);
+      }
+    }
+    return basename + outExt;
+  }
+
+  private Options createOptions(File outputFile) {
+    OptionsBuilder optionsBuilder = OptionsBuilder.options();
+
+    optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY)
+      .safe(SafeMode.UNSAFE);
+    // XXX(fishywang): ideally we should just output to a string and add the
+    // content into zip. But asciidoctor will actually ignore all attributes if
+    // not output to a file. So we *have* to output to a file then read the
+    // content of the file into zip.
+    optionsBuilder.toFile(outputFile);
+
+    AttributesBuilder attributesBuilder = AttributesBuilder.attributes();
+    attributesBuilder.attributes(getAttributes());
+    optionsBuilder.attributes(attributesBuilder.get());
+
+    return optionsBuilder.get();
+  }
+
+  private Map<String, Object> getAttributes() {
+    Map<String, Object> attributeValues = new HashMap<String, Object>();
+
+    for (String attribute : attributes) {
+      int equalsIndex = attribute.indexOf('=');
+      if(equalsIndex > -1) {
+        String name = attribute.substring(0, equalsIndex);
+        String value = attribute.substring(equalsIndex + 1, attribute.length());
+
+        attributeValues.put(name, value);
+      } else {
+        attributeValues.put(attribute, "");
+      }
+    }
+
+    return attributeValues;
+  }
+
+  private void invoke(String... parameters) throws IOException {
+    CmdLineParser parser = new CmdLineParser(this);
+    try {
+      parser.parseArgument(parameters);
+      if (inputFiles.isEmpty()) {
+        throw new CmdLineException(parser,
+            "asciidoctor: FAILED: input file missing");
+      }
+    } catch (CmdLineException e) {
+      System.err.println(e.getMessage());
+      parser.printUsage(System.err);
+      System.exit(1);
+      return;
+    }
+
+    ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile));
+    for (String inputFile : inputFiles) {
+      if (!inputFile.endsWith(inExt)) {
+        // We have to use UNSAFE mode in order to make embedding work. But in
+        // UNSAFE mode we'll also need css file in the same directory, so we
+        // have to add css files into the SRCS.
+        continue;
+      }
+      String outName = mapInFileToOutFile(inputFile, inExt, outExt);
+      File out = new File(outName);
+      Options options = createOptions(out);
+      renderInput(options, inputFile);
+
+      zipFile(out, outName, zip);
+    }
+    zip.close();
+  }
+
+  public static void zipDir(File dir, String prefix, ZipOutputStream zip)
+      throws IOException {
+    for (File file : dir.listFiles()) {
+      String name = file.getName();
+      if (!prefix.isEmpty()) {
+        name = prefix + "/" + name;
+      }
+      if (file.isDirectory()) {
+        zipDir(file, name, zip);
+      } else {
+        zipFile(file, name, zip);
+      }
+    }
+  }
+
+  public static void zipFile(File file, String name, ZipOutputStream zip)
+      throws IOException {
+    zip.putNextEntry(new ZipEntry(name));
+    FileInputStream input = new FileInputStream(file);
+    ByteStreams.copy(input, zip);
+    input.close();
+    zip.closeEntry();
+  }
+
+  private void renderInput(Options options, String inputFile) {
+    Asciidoctor asciidoctor = JRubyAsciidoctor.create();
+    asciidoctor.renderFile(new File(inputFile), options);
+  }
+
+  public static void main(String[] args) {
+    try {
+      new AsciiDoctor().invoke(args);
+    } catch (IOException e) {
+      System.err.println(e.getMessage());
+      System.exit(1);
+    }
+  }
+}
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
new file mode 100644
index 0000000..497cba5
--- /dev/null
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -0,0 +1,125 @@
+// 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.
+
+import com.google.common.io.Files;
+
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.analysis.util.CharArraySet;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.store.NIOFSDirectory;
+import org.apache.lucene.util.Version;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipOutputStream;
+
+public class DocIndexer {
+  private static final Version LUCENE_VERSION = Version.LUCENE_44;
+  private static final String DOC_FIELD = "doc";
+  private static final String URL_FIELD = "url";
+  private static final String TITLE_FIELD = "title";
+
+  @Option(name = "-z", usage = "output zip file")
+  private String zipFile;
+
+  @Option(name = "--prefix", usage = "prefix for the html filepath")
+  private String prefix = "";
+
+  @Option(name = "--in-ext", usage = "extension for input files")
+  private String inExt = ".txt";
+
+  @Option(name = "--out-ext", usage = "extension for output files")
+  private String outExt = ".html";
+
+  @Argument(usage = "input files")
+  private List<String> inputFiles = new ArrayList<String>();
+
+  private void invoke(String... parameters) throws IOException {
+    CmdLineParser parser = new CmdLineParser(this);
+    try {
+      parser.parseArgument(parameters);
+      if (inputFiles.isEmpty()) {
+        throw new CmdLineException(parser, "FAILED: input file missing");
+      }
+    } catch (CmdLineException e) {
+      System.err.println(e.getMessage());
+      parser.printUsage(System.err);
+      System.exit(1);
+      return;
+    }
+
+    File tmp = Files.createTempDir();
+    NIOFSDirectory directory = new NIOFSDirectory(tmp);
+    IndexWriterConfig config = new IndexWriterConfig(
+        LUCENE_VERSION,
+        new StandardAnalyzer(LUCENE_VERSION, CharArraySet.EMPTY_SET));
+    config.setOpenMode(OpenMode.CREATE);
+    IndexWriter iwriter = new IndexWriter(directory, config);
+    for (String inputFile : inputFiles) {
+      File file = new File(inputFile);
+
+      BufferedReader titleReader = new BufferedReader(
+          new InputStreamReader(new FileInputStream(file), "UTF-8"));
+      String title = titleReader.readLine();
+      if (title != null && title.startsWith("[[")) {
+        // Generally the first line of the txt is the title. In a few cases the
+        // first line is a "[[tag]]" and the second line is the title.
+        title = titleReader.readLine();
+      }
+      titleReader.close();
+
+      String outputFile = AsciiDoctor.mapInFileToOutFile(
+          inputFile, inExt, outExt);
+      FileReader reader = new FileReader(file);
+      Document doc = new Document();
+      doc.add(new TextField(DOC_FIELD, reader));
+      doc.add(new StringField(
+            URL_FIELD, prefix + outputFile, Field.Store.YES));
+      doc.add(new TextField(TITLE_FIELD, title, Field.Store.YES));
+      iwriter.addDocument(doc);
+      reader.close();
+    }
+    iwriter.close();
+
+    ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile));
+    AsciiDoctor.zipDir(tmp, "", zip);
+    zip.close();
+  }
+
+  public static void main(String[] args) {
+    try {
+      new DocIndexer().invoke(args);
+    } catch (IOException e) {
+      System.err.println(e.getMessage());
+      System.exit(1);
+    }
+  }
+}
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK
new file mode 100644
index 0000000..3d76ea6
--- /dev/null
+++ b/lib/bouncycastle/BUCK
@@ -0,0 +1,20 @@
+include_defs('//lib/maven.defs')
+
+# This version must match the version that also appears in
+# gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
+VERSION = '1.44'
+
+maven_jar(
+  name = 'bcprov',
+  id = 'org.bouncycastle:bcprov-jdk16:' + VERSION,
+  sha1 = '6327a5f7a3dc45e0fd735adb5d08c5a74c05c20c',
+  license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
+)
+
+maven_jar(
+  name = 'bcpg',
+  id = 'org.bouncycastle:bcpg-jdk16:' + VERSION,
+  sha1 = 'ee14f5a29cb3cf9c1edec034ab16e1bbd26e9647',
+  license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
+  deps = [':bcprov'],
+)
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
new file mode 100644
index 0000000..fcb6e92
--- /dev/null
+++ b/lib/codemirror/BUCK
@@ -0,0 +1,89 @@
+include_defs('//lib/maven.defs')
+include_defs('//lib/codemirror/cm3.defs')
+
+VERSION = 'a31c335e'
+SHA1 = '40fcf309c8df4228f94d53e5665c2b6588819a2d'
+URL = GERRIT + 'net/codemirror/codemirror-%s.zip' % VERSION
+
+ZIP = 'codemirror-%s.zip' % VERSION
+TOP = 'codemirror-%s' % VERSION
+
+genrule(
+  name = 'css',
+  cmd = ';'.join([
+      ':>$OUT',
+      "echo '/** @license' >>$OUT",
+      'unzip -p $SRCDIR/%s %s/LICENSE >>$OUT' % (ZIP, TOP),
+      "echo '*/' >>$OUT",
+    ] +
+    ['unzip -p $SRCDIR/%s %s/%s >>$OUT' % (ZIP, TOP, n)
+     for n in CM3_CSS]
+  ),
+  srcs = [genfile(ZIP)],
+  deps = [':download'],
+  out = 'cm3.css',
+)
+
+# TODO(sop) Minify with Closure JavaScript compiler.
+genrule(
+  name = 'js',
+  cmd = ';'.join([
+      ':>$OUT',
+      "echo '/** @license' >>$OUT",
+      'unzip -p $SRCDIR/%s %s/LICENSE >>$OUT' % (ZIP, TOP),
+      "echo '*/' >>$OUT",
+    ] +
+    ['unzip -p $SRCDIR/%s %s/%s >>$OUT' % (ZIP, TOP, n)
+     for n in CM3_JS]
+  ),
+  srcs = [genfile(ZIP)],
+  deps = [':download'],
+  out = 'cm3.js',
+)
+
+prebuilt_jar(
+  name = 'codemirror',
+  binary_jar = genfile('codemirror.jar'),
+  deps = [
+    ':jar',
+    '//lib:LICENSE-codemirror',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'jar',
+  cmd = ';'.join([
+    'cd $TMP',
+    'unzip -q $SRCDIR/%s %s' % (
+      ZIP,
+      ' '.join(['%s/mode/%s' % (TOP, n) for n in CM3_MODES])),
+    'mkdir net',
+    'mv %s net/codemirror' % TOP,
+    'mkdir net/codemirror/lib',
+    'mv $SRCDIR/cm3.css net/codemirror/lib',
+    'mv $SRCDIR/cm3.js net/codemirror/lib',
+    'zip -qr $OUT *'
+  ]),
+  srcs = [
+    genfile(ZIP),
+    genfile('cm3.css'),
+    genfile('cm3.js'),
+  ],
+  deps = [
+    ':download',
+    ':css',
+    ':js',
+  ],
+  out = 'codemirror.jar',
+)
+
+genrule(
+  name = 'download',
+  cmd = '$(exe //tools:download_file)' +
+    ' -o $OUT' +
+    ' -u ' + URL +
+    ' -v ' + SHA1,
+  deps = ['//tools:download_file'],
+  out = 'codemirror-' + VERSION + '.zip',
+)
diff --git a/lib/codemirror/cm3.defs b/lib/codemirror/cm3.defs
new file mode 100644
index 0000000..ef1851b
--- /dev/null
+++ b/lib/codemirror/cm3.defs
@@ -0,0 +1,28 @@
+CM3_CSS = [
+  'lib/codemirror.css',
+  'addon/dialog/dialog.css',
+]
+
+CM3_JS = [
+  'lib/codemirror.js',
+  'keymap/vim.js',
+  'addon/dialog/dialog.js',
+  'addon/search/searchcursor.js',
+  'addon/search/search.js',
+  'addon/selection/mark-selection.js',
+  'addon/edit/trailingspace.js',
+]
+
+CM3_MODES = [
+  'clike/clike.js',
+  'css/css.js',
+  'go/go.js',
+  'htmlmixed/htmlmixed.js',
+  'javascript/javascript.js',
+  'properties/properties.js',
+  'python/python.js',
+  'shell/shell.js',
+  'sql/sql.js',
+  'velocity/velocity.js',
+  'xml/xml.js',
+]
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
new file mode 100644
index 0000000..6f412e4
--- /dev/null
+++ b/lib/commons/BUCK
@@ -0,0 +1,105 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'codec',
+  id = 'commons-codec:commons-codec:1.4',
+  sha1 = '4216af16d38465bbab0f3dff8efa14204f7a399a',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+  name = 'collections',
+  id = 'commons-collections:commons-collections:3.2.1',
+  sha1 = '761ea405b9b37ced573d2df0d1e3a4e0f9edc668',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+  attach_source = False,
+  visibility = [
+    '//lib:velocity',
+    '//lib/solr:zookeeper',
+  ],
+)
+
+maven_jar(
+  name = 'dbcp',
+  id = 'commons-dbcp:commons-dbcp:1.4',
+  sha1 = '30be73c965cc990b153a100aaaaafcf239f82d39',
+  license = 'Apache2.0',
+  deps = [':pool'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+    'testpool.jocl'
+  ],
+)
+
+maven_jar(
+  name = 'lang',
+  id = 'commons-lang:commons-lang:2.5',
+  sha1 = 'b0236b252e86419eef20c31a44579d2aee2f0a69',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+  name = 'net',
+  id = 'commons-net:commons-net:2.2',
+  sha1 = '07993c12f63c78378f8c90de4bc2ee62daa7ca3a',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+  name = 'pool',
+  id = 'commons-pool:commons-pool:1.5.5',
+  sha1 = '7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b',
+  license = 'Apache2.0',
+  attach_source = False,
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+  name = 'oro',
+  id = 'oro:oro:2.0.8',
+  sha1 = '5592374f834645c4ae250f4c9fbb314c9369d698',
+  license = 'Apache1.1',
+  attach_source = False,
+  exclude = ['META-INF/LICENSE'],
+)
+
+maven_jar(
+  name = 'io',
+  id = 'commons-io:commons-io:1.4',
+  sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'httpclient',
+  id = 'org.apache.httpcomponents:httpclient:4.2.5',
+  bin_sha1 = '666e26e76f2e87d84e4f16acb546481ae1b8e9a6',
+  src_sha1 = '55d345272944d7e8dace47925336a3764ee0e24b',
+  license = 'Apache2.0',
+  deps = [
+    ':codec',
+    ':httpcore',
+    '//lib/log:jcl-over-slf4j',
+  ],
+)
+
+maven_jar(
+  name = 'httpcore',
+  id = 'org.apache.httpcomponents:httpcore:4.2.4',
+  bin_sha1 = '3b7f38df6de5dd8b500e602ae8c2dd5ee446f883',
+  src_sha1 = 'c3ffe3a73348645042fb0b06303b6a3de194494e',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'httpmime',
+  id = 'org.apache.httpcomponents:httpmime:4.2.5',
+  bin_sha1 = '412b9914d0adec6d5716df1ada8acbc4f6f2dd37',
+  src_sha1 = 'c07ce7f6b153284a9ebaf58532c2442200cf3aa2',
+  license = 'Apache2.0',
+)
diff --git a/lib/guice/BUCK b/lib/guice/BUCK
new file mode 100644
index 0000000..4fac2c0
--- /dev/null
+++ b/lib/guice/BUCK
@@ -0,0 +1,96 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '4.0-beta'
+EXCLUDE = [
+  'META-INF/DEPENDENCIES',
+  'META-INF/LICENSE',
+  'META-INF/NOTICE',
+]
+
+java_library(
+  name = 'guice',
+  deps = [
+    ':guice_library',
+    ':javax-inject',
+  ],
+  export_deps = True,
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'guice_library',
+  id = 'com.google.inject:guice:' + VERSION,
+  sha1 = 'a82be989679df08b66d48b42659a3ca2daaf1d5b',
+  license = 'Apache2.0',
+  deps = [':aopalliance'],
+  exclude = EXCLUDE + [
+    'META-INF/maven/com.google.guava/guava/pom.properties',
+    'META-INF/maven/com.google.guava/guava/pom.xml',
+    'javax/annotation/CheckForNull.java',
+    'javax/annotation/CheckForSigned.java',
+    'javax/annotation/CheckReturnValue.java',
+    'javax/annotation/concurrent/GuardedBy.java',
+    'javax/annotation/concurrent/Immutable.java',
+    'javax/annotation/concurrent/NotThreadSafe.java',
+    'javax/annotation/concurrent/ThreadSafe.java',
+    'javax/annotation/Detainted.java',
+    'javax/annotation/MatchesPattern.java',
+    'javax/annotation/meta/Exclusive.java',
+    'javax/annotation/meta/Exhaustive.java',
+    'javax/annotation/meta/TypeQualifier.java',
+    'javax/annotation/meta/TypeQualifierDefault.java',
+    'javax/annotation/meta/TypeQualifierNickname.java',
+    'javax/annotation/meta/TypeQualifierValidator.java',
+    'javax/annotation/meta/When.java',
+    'javax/annotation/Nonnegative.java',
+    'javax/annotation/Nonnull.java',
+    'javax/annotation/Nullable.java',
+    'javax/annotation/OverridingMethodsMustInvokeSuper.java',
+    'javax/annotation/ParametersAreNonnullByDefault.java',
+    'javax/annotation/ParametersAreNullableByDefault.java',
+    'javax/annotation/PropertyKey.java',
+    'javax/annotation/RegEx.java',
+    'javax/annotation/Signed.java',
+    'javax/annotation/Syntax.java',
+    'javax/annotation/Tainted.java',
+    'javax/annotation/Untainted.java',
+    'javax/annotation/WillClose.java',
+    'javax/annotation/WillCloseWhenClosed.java',
+    'javax/annotation/WillNotClose.java',
+  ],
+  visibility = [],
+)
+
+maven_jar(
+  name = 'guice-assistedinject',
+  id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION,
+  sha1 = 'abd6511011a9e4b64e2ebb60caac2e1cd6cd19a1',
+  license = 'Apache2.0',
+  deps = [':guice'],
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'guice-servlet',
+  id = 'com.google.inject.extensions:guice-servlet:' + VERSION,
+  sha1 = '46b44984f254c0bf92d0c972fad1a70292ada28e',
+  license = 'Apache2.0',
+  deps = [':guice'],
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'aopalliance',
+  id = 'aopalliance:aopalliance:1.0',
+  sha1 = '0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8',
+  license = 'PublicDomain',
+  visibility = ['//lib/guice:guice'],
+)
+
+maven_jar(
+  name = 'javax-inject',
+  id = 'javax.inject:javax.inject:1',
+  sha1 = '6975da39a7040257bd51d21a231b76c915872d38',
+  license = 'Apache2.0',
+  visibility = ['//lib/guice:guice'],
+)
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK
new file mode 100644
index 0000000..0ccb22b
--- /dev/null
+++ b/lib/gwt/BUCK
@@ -0,0 +1,59 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '2.5.1'
+
+maven_jar(
+  name = 'user',
+  id = 'com.google.gwt:gwt-user:' + VERSION,
+  sha1 = 'a8afe9b0222db730f4ebd02a1aa329a5395473c5',
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'dev',
+  id = 'com.google.gwt:gwt-dev:' + VERSION,
+  sha1 = 'ba1f05ddd23b51c0d9c813956ca0ea72cb2e7a92',
+  license = 'Apache2.0',
+  deps = [
+    ':javax-validation',
+    ':javax-validation_src',
+  ],
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'javax-validation',
+  id = 'javax.validation:validation-api:1.0.0.GA',
+  bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
+  src_sha1 = '7a561191db2203550fbfa40d534d4997624cd369',
+  license = 'Apache2.0',
+  visibility = [],
+)
+
+python_binary(
+  name = 'compiler',
+  main = 'compiler.py',
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'gwt-test-utils',
+  id = 'com.googlecode.gwt-test-utils:gwt-test-utils:0.45',
+  sha1 = 'ed16fa85defc685802e11cc61f8bc70454412fdb',
+  license = 'Apache2.0',
+  deps = [
+    ':javassist',
+    '//lib/log:api',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'javassist',
+  id = 'org.javassist:javassist:3.16.1-GA',
+  sha1 = '315891b371395271977af518d4db5cee1a0bc9bf',
+  license = 'Apache2.0',
+  visibility = [],
+)
+
diff --git a/lib/gwt/compiler.py b/lib/gwt/compiler.py
new file mode 100755
index 0000000..f7b478c
--- /dev/null
+++ b/lib/gwt/compiler.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+# 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.
+
+from __future__ import print_function
+
+from multiprocessing import cpu_count
+from os import makedirs, mkdir, path
+from subprocess import Popen, PIPE
+from sys import argv, stderr
+
+cp, opt, end = [], [], False
+module, TMP, outzip = argv[1], argv[2], argv[3]
+
+for a in argv[4:]:
+  if end:
+    if a.endswith('.jar'):
+      cp.append(path.expandvars(a))
+  elif a == '--':
+    end = True
+  else:
+    opt.append(a)
+
+if not outzip.endswith('.zip'):
+  print("%s must end with .zip" % outzip, file=stderr)
+  exit(1)
+
+for d in ['deploy', 'unit_cache', 'work']:
+  mkdir(path.join(TMP, d))
+if not path.exists(path.dirname(outzip)):
+  makedirs(path.dirname(outzip))
+
+cmd = [
+  'java', '-Xmx512m',
+  '-Djava.io.tmpdir=' + TMP,
+  '-Dgwt.normalizeTimestamps=true',
+  '-Dgwt.persistentunitcachedir=' + path.join(TMP, 'unit_cache'),
+  '-classpath', ':'.join(cp),
+  'com.google.gwt.dev.Compiler',
+  '-deploy', path.join(TMP, 'deploy'),
+  '-workDir', path.join(TMP, 'work'),
+  '-war', outzip,
+  '-localWorkers', str(cpu_count()),
+] + opt + [module]
+
+try:
+  gwt = Popen(cmd, stdout=PIPE, stderr=PIPE)
+  out, err = gwt.communicate()
+  if gwt.returncode != 0:
+    print(out + err, file=stderr)
+    exit(gwt.returncode)
+except KeyboardInterrupt:
+  print("Interrupted by user", file=stderr)
+  exit(1)
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
new file mode 100644
index 0000000..6eac1a9
--- /dev/null
+++ b/lib/jetty/BUCK
@@ -0,0 +1,75 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '8.1.7.v20120910'
+EXCLUDE = ['about.html']
+
+maven_jar(
+  name = 'servlet',
+  id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
+  sha1 = '93da01e3ea26e70449e9a1a0affa5c31436be5a0',
+  license = 'Apache2.0',
+  deps = [
+    ':security',
+    '//lib:servlet-api-3_0',
+  ],
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'security',
+  id = 'org.eclipse.jetty:jetty-security:' + VERSION,
+  sha1 = '8d78beb7a07f4cccee05a3f16a264f1025946258',
+  license = 'Apache2.0',
+  deps = [':server'],
+  exclude = EXCLUDE,
+  visibility = [],
+)
+
+maven_jar(
+  name = 'server',
+  id = 'org.eclipse.jetty:jetty-server:' + VERSION,
+  sha1 = '6c81f733f28713919e99c2f8952e6ca5178033cd',
+  license = 'Apache2.0',
+  deps = [
+    ':continuation',
+    ':http',
+  ],
+  export_deps = True,
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'continuation',
+  id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
+  sha1 = 'f60cfe6267038000b459508529c88737601081e4',
+  license = 'Apache2.0',
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'http',
+  id = 'org.eclipse.jetty:jetty-http:' + VERSION,
+  sha1 = '10126433876cd74534695f7f99c4362596555493',
+  license = 'Apache2.0',
+  deps = [':io'],
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'io',
+  id = 'org.eclipse.jetty:jetty-io:' + VERSION,
+  sha1 = 'a81f746ae1b10c37e1bb0a01d1374c202c0bd549',
+  license = 'Apache2.0',
+  deps = [':util'],
+  exclude = EXCLUDE,
+  visibility = [],
+)
+
+maven_jar(
+  name = 'util',
+  id = 'org.eclipse.jetty:jetty-util:' + VERSION,
+  sha1 = '7eb2004ab2c22fd3b00095bd9ba0f32a9e88f6a5',
+  license = 'Apache2.0',
+  exclude = EXCLUDE,
+  visibility = [],
+)
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
new file mode 100644
index 0000000..3e481bf
--- /dev/null
+++ b/lib/jgit/BUCK
@@ -0,0 +1,66 @@
+include_defs('//lib/maven.defs')
+
+REPO = ECLIPSE
+VERS = '3.1.0.201310021548-r'
+
+maven_jar(
+  name = 'jgit',
+  id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
+  bin_sha1 = 'df1410e5d1deaacfb70a2441b4766b61f2795bc3',
+  src_sha1 = 'b4e3d9c9c3da39b72acf72bd913ce9dbee88a9d4',
+  license = 'jgit',
+  repository = REPO,
+  unsign = True,
+  deps = [':ewah'],
+  exclude = [
+    'META-INF/eclipse.inf',
+    'about.html',
+    'plugin.properties',
+  ],
+)
+
+maven_jar(
+  name = 'jgit-servlet',
+  id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
+  sha1 = 'bcac91120afac59c195230537bde07175578fe79',
+  license = 'jgit',
+  repository = REPO,
+  deps = [':jgit'],
+  exclude = [
+    'about.html',
+    'plugin.properties',
+  ],
+)
+
+maven_jar(
+  name = 'junit',
+  id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
+  sha1 = 'a8b47bb41cec25b1d128f7d267badbc7dcf6d9aa',
+  license = 'DO_NOT_DISTRIBUTE',
+  repository = REPO,
+  deps = [':jgit'],
+)
+
+maven_jar(
+  name = 'ewah',
+  id = 'com.googlecode.javaewah:JavaEWAH:0.5.6',
+  sha1 = '1207c0fc8552d4f5f574b50f29321d923521128e',
+  license = 'Apache2.0',
+)
+
+prebuilt_jar(
+  name = 'Edit',
+  binary_jar = genfile('edit-src.jar'),
+  deps = [':jgit_edit_src'],
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'jgit_edit_src',
+  cmd = 'unzip -qd $TMP $SRCS org/eclipse/jgit/diff/Edit.java;' +
+    'cd $TMP;' +
+    'zip -Dq $OUT org/eclipse/jgit/diff/Edit.java',
+  srcs = [genfile('jgit/org.eclipse.jgit-%s-src.jar' % VERS)],
+  out = 'edit-src.jar',
+  deps = [':jgit_src']
+)
diff --git a/lib/joda/BUCK b/lib/joda/BUCK
new file mode 100644
index 0000000..d45ce78
--- /dev/null
+++ b/lib/joda/BUCK
@@ -0,0 +1,25 @@
+include_defs('//lib/maven.defs')
+
+EXCLUDE = [
+  'META-INF/LICENSE.txt',
+  'META-INF/NOTICE.txt',
+]
+
+maven_jar(
+  name = 'joda-time',
+  id = 'joda-time:joda-time:2.3',
+  sha1 = '56498efd17752898cfcc3868c1b6211a07b12b8f',
+  deps = [':joda-convert'],
+  license = 'Apache2.0',
+  exclude = EXCLUDE,
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'joda-convert',
+  id = 'org.joda:joda-convert:1.2',
+  bin_sha1 = '35ec554f0cd00c956cc69051514d9488b1374dec',
+  license = 'Apache2.0',
+  exclude = EXCLUDE,
+  visibility = ['//lib/joda:joda-time'],
+)
diff --git a/lib/log/BUCK b/lib/log/BUCK
new file mode 100644
index 0000000..2659fcd
--- /dev/null
+++ b/lib/log/BUCK
@@ -0,0 +1,31 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'api',
+  id = 'org.slf4j:slf4j-api:1.6.1',
+  sha1 = '6f3b8a24bf970f17289b234284c94f43eb42f0e4',
+  license = 'slf4j',
+)
+
+maven_jar(
+  name = 'impl_log4j',
+  id = 'org.slf4j:slf4j-log4j12:1.6.1',
+  sha1 = 'bd245d6746cdd4e6203e976e21d597a46f115802',
+  license = 'slf4j',
+  deps = [':log4j'],
+)
+
+maven_jar(
+  name = 'log4j',
+  id = 'log4j:log4j:1.2.16',
+  sha1 = '7999a63bfccbc7c247a9aea10d83d4272bd492c6',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
+)
+
+maven_jar(
+  name = 'jcl-over-slf4j',
+  id = 'org.slf4j:jcl-over-slf4j:1.6.1',
+  sha1 = '99c61095a14dfc9e47a086068033c286bf236475',
+  license = 'slf4j',
+)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
new file mode 100644
index 0000000..38ece4c
--- /dev/null
+++ b/lib/lucene/BUCK
@@ -0,0 +1,49 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'core',
+  id = 'org.apache.lucene:lucene-core:4.4.0',
+  bin_sha1 = 'a9a0b553d5f2444aea3340b22753ea4bbddaa0af',
+  src_sha1 = 'd321e15f688066a3c3598607303e0de452a076da',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'analyzers-common',
+  id = 'org.apache.lucene:lucene-analyzers-common:4.4.0',
+  bin_sha1 = 'f58f6b727293b2d4392064db8c91fdf1d0eb4ffe',
+  src_sha1 = '60176bb63009f41104b42656b20c81b66313e7b5',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'highlighter',
+  id = 'org.apache.lucene:lucene-highlighter:4.4.0',
+  bin_sha1 = 'c55f402683388c0a71a1dfaaff198873dfe5b1e4',
+  src_sha1 = '3a99f84e4b6a8f74c34b2f2bd076c9b2b46fff2e',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'queries',
+  id = 'org.apache.lucene:lucene-queries:4.4.0',
+  bin_sha1 = 'c9010f4852345ba2a65163fdeb17b7b653e4a3c4',
+  src_sha1 = 'eefbcd43e66747a412a9f186d183d187405374b8',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'spellchecker',
+  id = 'org.apache.lucene:lucene-spellchecker:3.6.2',
+  bin_sha1 = '15db0c0cfee44e275f15ad046e46b9a05910ad24',
+  src_sha1 = 'bbecb3fb725ae594101c165a72c102296007c203',
+  license = 'Apache2.0',
+)
diff --git a/lib/maven.defs b/lib/maven.defs
new file mode 100644
index 0000000..54840e8
--- /dev/null
+++ b/lib/maven.defs
@@ -0,0 +1,132 @@
+# 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.
+
+GERRIT = 'GERRIT:'
+ECLIPSE = 'ECLIPSE:'
+MAVEN_CENTRAL = 'MAVEN_CENTRAL:'
+MAVEN_LOCAL = 'MAVEN_LOCAL:'
+
+def define_license(name):
+  n = 'LICENSE-' + name
+  genrule(
+    name = n,
+    cmd = 'ln -s $SRCS $OUT',
+    srcs = [n],
+    out = n,
+    visibility = ['PUBLIC'],
+  )
+
+def maven_jar(
+    name,
+    id,
+    license,
+    exclude = [],
+    exclude_java_sources = False,
+    unsign = False,
+    deps = [],
+    sha1 = '', bin_sha1 = '', src_sha1 = '',
+    repository = MAVEN_CENTRAL,
+    attach_source = True,
+    export_deps = False,
+    visibility = ['PUBLIC']):
+  from os import path
+
+  parts = id.split(':')
+  if len(parts) != 3:
+    raise NameError('expected id="groupId:artifactId:version"')
+  group, artifact, version = parts
+
+  if 'SNAPSHOT' in version:
+    file_version = version.replace('-SNAPSHOT', '')
+    version = version.split('-SNAPSHOT')[0] + '-SNAPSHOT'
+  else:
+    file_version = version
+
+  jar = path.join(name, artifact.lower() + '-' + file_version)
+  url = '/'.join([
+    repository,
+    group.replace('.', '/'), artifact, version,
+    artifact + '-' + file_version])
+
+  binjar = jar + '.jar'
+  binurl = url + '.jar'
+
+  srcjar = jar + '-src.jar'
+  srcurl = url + '-sources.jar'
+
+  cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', binurl]
+  if sha1:
+    cmd.extend(['-v', sha1])
+  elif bin_sha1:
+    cmd.extend(['-v', bin_sha1])
+  for x in exclude:
+    cmd.extend(['-x', x])
+  if exclude_java_sources:
+    cmd.append('--exclude_java_sources')
+  if unsign:
+    cmd.append('--unsign')
+
+  genrule(
+    name = name + '__download_bin',
+    cmd = ' '.join(cmd),
+    deps = ['//tools:download_file'],
+    out = binjar,
+  )
+  license = ['//lib:LICENSE-' + license]
+
+  if src_sha1 or attach_source:
+    cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', srcurl]
+    if src_sha1:
+      cmd.extend(['-v', src_sha1])
+    genrule(
+      name = name + '__download_src',
+      cmd = ' '.join(cmd),
+      deps = ['//tools:download_file'],
+      out = srcjar,
+    )
+    prebuilt_jar(
+      name = name + '_src',
+      binary_jar = genfile(srcjar),
+      deps = license + [':' + name + '__download_src'],
+      visibility = visibility,
+    )
+  else:
+    srcjar = None
+    genrule(
+      name = name + '__download_src',
+      cmd = ':>$OUT',
+      out = '__' + name + '__no_src',
+    )
+
+  if export_deps:
+    prebuilt_jar(
+      name = name + '__jar',
+      deps = deps + license + [':' + name + '__download_bin'],
+      binary_jar = genfile(binjar),
+      source_jar = genfile(srcjar) if srcjar else None,
+    )
+    java_library(
+      name = name,
+      deps = [':' + name + '__jar'],
+      export_deps = True,
+      visibility = visibility,
+    )
+  else:
+    prebuilt_jar(
+      name = name,
+      deps = deps + license + [':' + name + '__download_bin'],
+      binary_jar = genfile(binjar),
+      source_jar = genfile(srcjar) if srcjar else None,
+      visibility = visibility,
+    )
diff --git a/lib/mina/BUCK b/lib/mina/BUCK
new file mode 100644
index 0000000..9467cc4
--- /dev/null
+++ b/lib/mina/BUCK
@@ -0,0 +1,25 @@
+include_defs('//lib/maven.defs')
+
+EXCLUDE = [
+  'META-INF/DEPENDENCIES',
+  'META-INF/LICENSE',
+  'META-INF/NOTICE',
+]
+
+maven_jar(
+  name = 'core',
+  id = 'org.apache.mina:mina-core:2.0.7',
+  sha1 = 'c878e2aa82de748474a624ec3933e4604e446dec',
+  license = 'Apache2.0',
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'sshd',
+  id = 'org.apache.sshd:sshd-core:0.9.0.201311081',
+  sha1 = '38f7ac8602e70fa05fdc6147d204198e9cefe5bc',
+  license = 'Apache2.0',
+  deps = [':core'],
+  exclude = EXCLUDE,
+  repository = GERRIT,
+)
diff --git a/lib/openid/BUCK b/lib/openid/BUCK
new file mode 100644
index 0000000..c6c8baf
--- /dev/null
+++ b/lib/openid/BUCK
@@ -0,0 +1,35 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'consumer',
+  id = 'org.openid4java:openid4java:0.9.8',
+  sha1 = 'de4f1b33d3b0f0b2ab1d32834ec1190b39db4160',
+  license = 'Apache2.0',
+  deps = [
+    ':nekohtml',
+    ':xerces',
+    '//lib/commons:httpclient',
+    '//lib/log:jcl-over-slf4j',
+    '//lib/guice:guice',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'nekohtml',
+  id = 'net.sourceforge.nekohtml:nekohtml:1.9.10',
+  sha1 = '14052461031a7054aa094f5573792feb6686d3de',
+  license = 'Apache2.0',
+  deps = [':xerces'],
+  attach_source = False,
+  visibility = [],
+)
+
+maven_jar(
+  name = 'xerces',
+  id = 'xerces:xercesImpl:2.8.1',
+  sha1 = '25101e37ec0c907db6f0612cbf106ee519c1aef1',
+  license = 'Apache2.0',
+  attach_source = False,
+  visibility = [],
+)
diff --git a/lib/prolog/BUCK b/lib/prolog/BUCK
new file mode 100644
index 0000000..1f3e425
--- /dev/null
+++ b/lib/prolog/BUCK
@@ -0,0 +1,23 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'prolog-cafe',
+  id = 'com.googlecode.prolog-cafe:PrologCafe:1.3',
+  sha1 = '5e0fbf18e8c98c4113f9acc978306884a1152870',
+  license = 'prologcafe',
+  repository = GERRIT,
+)
+
+java_binary(
+  name = 'compiler',
+  main_class = 'BuckPrologCompiler',
+  deps = [':compiler_lib'],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'compiler_lib',
+  srcs = ['java/BuckPrologCompiler.java'],
+  deps = [':prolog-cafe'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/lib/prolog/java/BuckPrologCompiler.java b/lib/prolog/java/BuckPrologCompiler.java
new file mode 100644
index 0000000..0db6763
--- /dev/null
+++ b/lib/prolog/java/BuckPrologCompiler.java
@@ -0,0 +1,86 @@
+// 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.
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.compiler.Compiler;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+public class BuckPrologCompiler {
+  public static void main(String[] argv) throws IOException, CompileException {
+    File out = new File(argv[argv.length - 1]);
+    File java = tmpdir("java");
+    for (int i = 0; i < argv.length - 1; i++) {
+      File src = new File(argv[i]);
+      new Compiler().prologToJavaSource(src.getPath(), java.getPath());
+    }
+    jar(out, java);
+  }
+
+  private static File tmpdir(String name) throws IOException {
+    File d = File.createTempFile(name + "_", "");
+    if (!d.delete() || !d.mkdir()) {
+      throw new IOException("Cannot mkdir " + d);
+    }
+    return d;
+  }
+
+  private static void jar(File jar, File classes) throws IOException {
+    File tmp = File.createTempFile("prolog", ".jar", jar.getParentFile());
+    try {
+      JarOutputStream out = new JarOutputStream(new FileOutputStream(tmp));
+      try {
+        add(out, classes, "");
+      } finally {
+        out.close();
+      }
+      if (!tmp.renameTo(jar)) {
+        throw new IOException("Cannot create " + jar);
+      }
+    } finally {
+      tmp.delete();
+    }
+  }
+
+  private static void add(JarOutputStream out, File classes, String prefix)
+      throws IOException {
+    for (String name : classes.list()) {
+      File f = new File(classes, name);
+      if (f.isDirectory()) {
+        add(out, f, prefix + name + "/");
+        continue;
+      }
+
+      JarEntry e = new JarEntry(prefix + name);
+      FileInputStream in = new FileInputStream(f);
+      try {
+        e.setTime(f.lastModified());
+        out.putNextEntry(e);
+        byte[] buf = new byte[16 << 10];
+        int n;
+        while (0 < (n = in.read(buf))) {
+          out.write(buf, 0, n);
+        }
+      } finally {
+        in.close();
+        out.closeEntry();
+      }
+    }
+  }
+}
diff --git a/lib/prolog/prolog.defs b/lib/prolog/prolog.defs
new file mode 100644
index 0000000..d8df8f7
--- /dev/null
+++ b/lib/prolog/prolog.defs
@@ -0,0 +1,48 @@
+# 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.
+
+def prolog_cafe_library(
+    name,
+    srcs,
+    deps = [],
+    visibility = []):
+  genrule(
+    name = name + '__pl2j',
+    cmd = 'cd $SRCDIR;$(exe //lib/prolog:compiler) ' +
+      ' '.join(srcs) +
+      ' $OUT',
+    srcs = srcs,
+    deps = ['//lib/prolog:compiler'],
+    out = name + '.src.zip',
+  )
+  java_library(
+    name = name + '__lib',
+    srcs = [genfile(name + '.src.zip')],
+    deps = [
+      ':' + name + '__pl2j',
+      '//lib/prolog:prolog-cafe',
+    ] + deps,
+  )
+  genrule(
+    name = name + '__ln',
+    cmd = 'ln -s $(location :%s__lib) $OUT' % name,
+    deps = [':%s__lib' % name],
+    out = name + '.jar',
+  )
+  prebuilt_jar(
+    name = name,
+    binary_jar = genfile(name + '.jar'),
+    deps = [':%s__ln' % name],
+    visibility = visibility,
+  )
diff --git a/lib/solr/BUCK b/lib/solr/BUCK
new file mode 100644
index 0000000..afaa948
--- /dev/null
+++ b/lib/solr/BUCK
@@ -0,0 +1,33 @@
+include_defs('//lib/maven.defs')
+
+# Java client library to use Solr over the network.
+maven_jar(
+  name = 'solrj',
+  id = 'org.apache.solr:solr-solrj:4.3.1',
+  sha1 = '433fe37796e67eaeb4452f69eb1fae2de27cb7a8',
+  license = 'Apache2.0',
+  deps = [
+    ':noggit',
+    ':zookeeper',
+    '//lib/commons:httpclient',
+    '//lib/commons:httpmime',
+    '//lib/commons:io',
+  ],
+)
+
+maven_jar(
+  name = 'noggit',
+  id = 'org.noggit:noggit:0.5',
+  sha1 = '8e6e65624d2e09a30190c6434abe23b7d4e5413c',
+  license = 'Apache2.0',
+  visibility = [],
+)
+
+maven_jar(
+  name = 'zookeeper',
+  id = 'org.apache.zookeeper:zookeeper:3.4.5',
+  sha1 = 'c0f69fb36526552a8f0bc548a6c33c49cf08e562',
+  license = 'Apache2.0',
+  deps = ['//lib/log:api'],
+  visibility = [],
+)
diff --git a/plugins/BUCK b/plugins/BUCK
new file mode 100644
index 0000000..6440fb7
--- /dev/null
+++ b/plugins/BUCK
@@ -0,0 +1,37 @@
+BASE = get_base_path()
+CORE = [
+  'commit-message-length-validator',
+  'download-commands',
+  'replication',
+  'reviewnotes',
+]
+
+# buck audit parses and resolves all deps even if not reachable
+# from the root(s) passed to audit. Filter dependencies to only
+# the ones that currently exist to allow buck to parse cleanly.
+# TODO(sop): buck should more lazily resolve deps
+def filter(names):
+  from os import path
+  h, n = [], []
+  for p in names:
+    if path.exists(path.join(BASE, p, 'BUCK')):
+      h.append(p)
+    else:
+      n.append(p)
+  return h, n
+HAVE, NEED = filter(CORE)
+
+genrule(
+  name = 'core',
+  cmd = '' +
+    ';'.join(['echo >&2 plugins/'+n+' is required.' for n in NEED]) +
+    (';echo >&2;exit 1;' if NEED else '') +
+    'mkdir -p $TMP/WEB-INF/plugins;' +
+    'for s in $SRCS;do ln -s $s $TMP/WEB-INF/plugins;done;' +
+    'cd $TMP;' +
+    'zip -qr $OUT .',
+  srcs = [genfile('%s/%s.jar' % (n, n)) for n in CORE],
+  deps = ['//%s/%s:%s' % (BASE, n, n) for n in HAVE],
+  out = 'core.zip',
+  visibility = ['//:release'],
+)
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index 23bde32..45f347d 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit 23bde32a6f67ef39f2a368851e3abbedc50db710
+Subproject commit 45f347d0e258ef7b871b046bfa96b9f902063b10
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
new file mode 160000
index 0000000..5ee3f28
--- /dev/null
+++ b/plugins/cookbook-plugin
@@ -0,0 +1 @@
+Subproject commit 5ee3f28739700e97b91231a2b694f3ba78065e86
diff --git a/plugins/download-commands b/plugins/download-commands
new file mode 160000
index 0000000..05975dd
--- /dev/null
+++ b/plugins/download-commands
@@ -0,0 +1 @@
+Subproject commit 05975dd6c8ca44de2e01cede16a94df49a3a825f
diff --git a/plugins/replication b/plugins/replication
index df8cb39..fa66d17 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit df8cb393676880b6e27b42d4b49492be0804c0ee
+Subproject commit fa66d17b1d6ed3266d0e9061a852fc530ec2ea73
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 8d2ab72..9905f7a 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 8d2ab72281506ab88307f69c433187fea31b6e89
+Subproject commit 9905f7af6c4e258365413a03f092898b167ea25a
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 7f6cc87..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,908 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2008 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-parent</artifactId>
-  <packaging>pom</packaging>
-  <version>2.7</version>
-
-  <name>Gerrit Code Review - Parent</name>
-  <url>http://code.google.com/p/gerrit/</url>
-
-  <description>
-    Gerrit - Web Based Code Review
-  </description>
-
-  <mailingLists>
-    <mailingList>
-      <name>repo-discuss mailing list</name>
-      <post>repo-discuss@googlegroups.com</post>
-      <archive>http://groups.google.com/group/repo-discuss</archive>
-      <subscribe>http://groups.google.com/group/repo-discuss/subscribe</subscribe>
-    </mailingList>
-  </mailingLists>
-
-  <issueManagement>
-    <url>http://code.google.com/p/gerrit/issues/list</url>
-    <system>Google Code</system>
-  </issueManagement>
-
-  <properties>
-    <jgitVersion>2.3.1.201302201838-r.209-g18030f9</jgitVersion>
-    <gwtormVersion>1.6</gwtormVersion>
-    <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
-    <gwtVersion>2.5.0</gwtVersion>
-    <bouncyCastleVersion>140</bouncyCastleVersion>
-    <slf4jVersion>1.6.1</slf4jVersion>
-    <guiceVersion>3.0</guiceVersion>
-    <jettyVersion>8.1.7.v20120910</jettyVersion>
-
-    <gwt.compileReport>false</gwt.compileReport>
-
-    <project.build.sourceEncoding>
-      UTF-8
-    </project.build.sourceEncoding>
-    <project.reporting.outputEncoding>
-      UTF-8
-    </project.reporting.outputEncoding>
-  </properties>
-
-  <modules>
-    <module>gerrit-patch-commonsnet</module>
-    <module>gerrit-patch-jgit</module>
-
-    <module>gerrit-util-cli</module>
-    <module>gerrit-util-ssl</module>
-
-    <module>gerrit-antlr</module>
-    <module>gerrit-common</module>
-    <module>gerrit-cache-h2</module>
-    <module>gerrit-httpd</module>
-    <module>gerrit-gwtexpui</module>
-    <module>gerrit-gwtui</module>
-    <module>gerrit-launcher</module>
-    <module>gerrit-main</module>
-    <module>gerrit-openid</module>
-    <module>gerrit-pgm</module>
-    <module>gerrit-prettify</module>
-    <module>gerrit-reviewdb</module>
-    <module>gerrit-server</module>
-    <module>gerrit-sshd</module>
-    <module>gerrit-gwtdebug</module>
-    <module>gerrit-war</module>
-
-    <module>gerrit-acceptance-tests</module>
-    <module>gerrit-extension-api</module>
-    <module>gerrit-plugin-api</module>
-    <module>gerrit-plugin-archetype</module>
-    <module>gerrit-plugin-gwtui</module>
-    <module>gerrit-plugin-js-archetype</module>
-    <module>gerrit-plugin-gwt-archetype</module>
-  </modules>
-
-  <profiles>
-    <profile>
-      <id>plugins</id>
-      <activation>
-        <property>
-          <name>!gerrit.plugins.skip</name>
-        </property>
-      </activation>
-      <modules>
-        <!-- CORE PLUGIN LIST -->
-        <module>plugins/commit-message-length-validator</module>
-        <module>plugins/replication</module>
-        <module>plugins/reviewnotes</module>
-      </modules>
-    </profile>
-  </profiles>
-
-  <licenses>
-    <license>
-      <name>Apache License, 2.0</name>
-      <comments>
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-      </comments>
-    </license>
-  </licenses>
-
-  <build>
-    <pluginManagement>
-      <plugins>
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-jar-plugin</artifactId>
-          <configuration>
-            <archive>
-              <manifestEntries>
-                <Implementation-Title>Gerrit Code Review - ${project.artifactId}</Implementation-Title>
-                <Implementation-Version>${project.version}</Implementation-Version>
-                <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
-                <Implementation-Vendor-Id>com.google.gerrit</Implementation-Vendor-Id>
-                <Implementation-Vendor-URL>http://code.google.com/p/gerrit/</Implementation-Vendor-URL>
-              </manifestEntries>
-            </archive>
-          </configuration>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-compiler-plugin</artifactId>
-          <version>2.3.2</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-source-plugin</artifactId>
-          <version>2.1.2</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-javadoc-plugin</artifactId>
-         <version>2.9.1</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-shade-plugin</artifactId>
-          <version>1.6</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-antrun-plugin</artifactId>
-          <version>1.6</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-war-plugin</artifactId>
-          <version>2.1.1</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-dependency-plugin</artifactId>
-          <version>2.5.1</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-surefire-plugin</artifactId>
-          <version>2.13</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.antlr</groupId>
-          <artifactId>antlr3-maven-plugin</artifactId>
-          <version>3.2</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.codehaus.mojo</groupId>
-          <artifactId>gwt-maven-plugin</artifactId>
-          <version>2.5.0</version>
-        </plugin>
-
-        <plugin>
-          <groupId>org.codehaus.mojo</groupId>
-          <artifactId>build-helper-maven-plugin</artifactId>
-          <version>1.5</version>
-        </plugin>
-
-        <!--This plugin's configuration is used to store Eclipse 
-            m2e settings only. It has no influence on the Maven build itself. -->
-        <plugin>
-          <groupId>org.eclipse.m2e</groupId>
-          <artifactId>lifecycle-mapping</artifactId>
-          <version>1.0.0</version>
-          <configuration>
-            <lifecycleMappingMetadata>
-              <pluginExecutions>
-                <pluginExecution>
-                  <pluginExecutionFilter>
-                    <groupId>org.apache.maven.plugins</groupId>
-                    <artifactId>maven-antrun-plugin</artifactId>
-                    <versionRange>[1.0,)</versionRange>
-                    <goals>
-                      <goal>run</goal>
-                    </goals>
-                  </pluginExecutionFilter>
-                  <action>
-                     <execute>
-                      <runOnIncremental>false</runOnIncremental>
-                      <runOnConfiguration>true</runOnConfiguration>
-                    </execute>
-                  </action>
-                </pluginExecution>
-                <pluginExecution>
-                  <pluginExecutionFilter>
-                    <groupId>org.codehaus.mojo</groupId>
-                    <artifactId>build-helper-maven-plugin</artifactId>
-                    <versionRange>[1.0,)</versionRange>
-                    <goals>
-                      <goal>add-source</goal>
-                    </goals>
-                  </pluginExecutionFilter>
-                  <action>
-                    <execute>
-                      <runOnIncremental>false</runOnIncremental>
-                      <runOnConfiguration>true</runOnConfiguration>
-                   </execute>
-                  </action>
-                </pluginExecution>
-              </pluginExecutions>
-            </lifecycleMappingMetadata>
-          </configuration>
-        </plugin>
-      </plugins>
-    </pluginManagement>
-
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <configuration>
-          <source>1.6</source>
-          <target>1.6</target>
-          <encoding>UTF-8</encoding>
-        </configuration>
-      </plugin>
-    </plugins>
-
-    <extensions>
-      <extension>
-        <groupId>com.googlesource.gerrit</groupId>
-        <artifactId>gs-maven-wagon</artifactId>
-        <version>3.3</version>
-      </extension>
-    </extensions>
-  </build>
-
-  <dependencies>
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <scope>test</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>org.easymock</groupId>
-      <artifactId>easymock</artifactId>
-      <scope>test</scope>
-    </dependency>
-  </dependencies>
-
-  <dependencyManagement>
-    <dependencies>
-      <dependency>
-        <groupId>com.google.code.gson</groupId>
-        <artifactId>gson</artifactId>
-        <version>2.1</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.guava</groupId>
-        <artifactId>guava</artifactId>
-        <version>14.0</version>
-      </dependency>
-
-      <dependency>
-        <groupId>gwtorm</groupId>
-        <artifactId>gwtorm</artifactId>
-        <version>${gwtormVersion}</version>
-      </dependency>
-      <dependency>
-        <groupId>gwtorm</groupId>
-        <artifactId>gwtorm</artifactId>
-        <version>${gwtormVersion}</version>
-        <classifier>sources</classifier>
-      </dependency>
-
-      <dependency>
-        <groupId>gwtjsonrpc</groupId>
-        <artifactId>gwtjsonrpc</artifactId>
-        <version>${gwtjsonrpcVersion}</version>
-      </dependency>
-      <dependency>
-        <groupId>gwtjsonrpc</groupId>
-        <artifactId>gwtjsonrpc</artifactId>
-        <version>${gwtjsonrpcVersion}</version>
-        <classifier>sources</classifier>
-      </dependency>
-
-      <dependency>
-        <groupId>org.openid4java</groupId>
-        <artifactId>openid4java</artifactId>
-        <version>0.9.8</version>
-        <exclusions>
-          <exclusion>
-            <!-- conflicts with our use of guice 3.0 -->
-            <groupId>com.google.code.guice</groupId>
-            <artifactId>guice</artifactId>
-          </exclusion>
-          <exclusion>
-            <!-- jug-1.1 is LGPL, and the source has been lost -->
-            <groupId>jug</groupId>
-            <artifactId>jug</artifactId>
-          </exclusion>
-          <exclusion>
-            <!-- not required on Java 5 or later -->
-            <groupId>xml-apis</groupId>
-            <artifactId>xml-apis</artifactId>
-          </exclusion>
-
-          <!-- optional, we aren't bothering with XRI support -->
-          <exclusion>
-            <groupId>org.openxri</groupId>
-            <artifactId>openxri</artifactId>
-          </exclusion>
-          <exclusion>
-            <groupId>xml-security</groupId>
-            <artifactId>xmlsec</artifactId>
-          </exclusion>
-          <exclusion>
-            <groupId>xalan</groupId>
-            <artifactId>xalan</artifactId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-
-      <dependency>
-        <groupId>org.apache.mina</groupId>
-        <artifactId>mina-core</artifactId>
-        <version>2.0.7</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.apache.sshd</groupId>
-        <artifactId>sshd-core</artifactId>
-        <version>0.9.0.201311081</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.jcraft</groupId>
-        <artifactId>jsch</artifactId>
-        <version>0.1.50</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.apache.velocity</groupId>
-        <artifactId>velocity</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-
-      <dependency>
-        <groupId>args4j</groupId>
-        <artifactId>args4j</artifactId>
-        <version>2.0.16</version>
-      </dependency>
-
-      <dependency>
-        <groupId>javax.validation</groupId>
-        <artifactId>validation-api</artifactId>
-        <version>1.0.0.GA</version>
-        <scope>provided</scope>
-      </dependency>
-      <dependency>
-        <groupId>javax.validation</groupId>
-        <artifactId>validation-api</artifactId>
-        <version>1.0.0.GA</version>
-        <classifier>sources</classifier>
-        <scope>provided</scope>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.inject</groupId>
-        <artifactId>guice</artifactId>
-        <version>${guiceVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.inject.extensions</groupId>
-        <artifactId>guice-servlet</artifactId>
-        <version>${guiceVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.inject.extensions</groupId>
-        <artifactId>guice-assistedinject</artifactId>
-        <version>${guiceVersion}</version>
-      </dependency>
-
-      <dependency>
-        <!-- required by Guice (whose POM is fake and lacks it) -->
-        <groupId>aopalliance</groupId>
-        <artifactId>aopalliance</artifactId>
-        <version>1.0</version>
-      </dependency>
-
-      <dependency>
-        <groupId>commons-net</groupId>
-        <artifactId>commons-net</artifactId>
-        <version>2.2</version>
-      </dependency>
-
-      <dependency>
-        <groupId>commons-codec</groupId>
-        <artifactId>commons-codec</artifactId>
-        <version>1.4</version>
-      </dependency>
-
-      <dependency>
-        <groupId>commons-dbcp</groupId>
-        <artifactId>commons-dbcp</artifactId>
-        <version>1.4</version>
-      </dependency>
-
-      <dependency>
-        <groupId>commons-pool</groupId>
-        <artifactId>commons-pool</artifactId>
-        <version>1.5.5</version>
-      </dependency>
-
-      <dependency>
-        <groupId>commons-lang</groupId>
-        <artifactId>commons-lang</artifactId>
-        <version>2.5</version>
-      </dependency>
-
-      <dependency>
-        <groupId>eu.medsea.mimeutil</groupId>
-        <artifactId>mime-util</artifactId>
-        <version>2.1.3</version>
-        <exclusions>
-          <exclusion>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-log4j12</artifactId>
-          </exclusion>
-          <exclusion>
-            <groupId>log4j</groupId>
-            <artifactId>log4j</artifactId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-
-      <dependency>
-        <groupId>org.antlr</groupId>
-        <artifactId>antlr</artifactId>
-        <version>3.2</version>
-        <exclusions>
-          <exclusion>
-            <groupId>org.antlr</groupId>
-            <artifactId>stringtemplate</artifactId>
-          </exclusion>
-          <exclusion>
-            <groupId>antlr</groupId>
-            <artifactId>antlr</artifactId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-
-      <dependency>
-        <groupId>bouncycastle</groupId>
-        <artifactId>bcpg-jdk15</artifactId>
-        <version>${bouncyCastleVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>bouncycastle</groupId>
-        <artifactId>bcprov-jdk15</artifactId>
-        <version>${bouncyCastleVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-api</artifactId>
-        <version>${slf4jVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-log4j12</artifactId>
-        <version>${slf4jVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>log4j</groupId>
-        <artifactId>log4j</artifactId>
-        <version>1.2.16</version>
-        <exclusions>
-          <exclusion>
-            <groupId>javax.mail</groupId>
-            <artifactId>mail</artifactId>
-          </exclusion>
-          <exclusion>
-            <groupId>javax.jms</groupId>
-            <artifactId>jms</artifactId>
-          </exclusion>
-          <exclusion>
-            <groupId>com.sun.jdmk</groupId>
-            <artifactId>jmxtools</artifactId>
-          </exclusion>
-          <exclusion>
-            <groupId>com.sun.jmx</groupId>
-            <artifactId>jmxri</artifactId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-
-      <dependency>
-        <groupId>org.eclipse.jgit</groupId>
-        <artifactId>org.eclipse.jgit</artifactId>
-        <version>${jgitVersion}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jgit</groupId>
-        <artifactId>org.eclipse.jgit</artifactId>
-        <version>${jgitVersion}</version>
-        <classifier>sources</classifier>
-      </dependency>
-
-      <dependency>
-        <groupId>org.eclipse.jgit</groupId>
-        <artifactId>org.eclipse.jgit.junit</artifactId>
-        <version>${jgitVersion}</version>
-        <scope>test</scope>
-        <exclusions>
-          <exclusion>
-            <groupId>org.eclipse.jgit</groupId>
-            <artifactId>org.eclipse.jgit</artifactId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-
-      <dependency>
-        <groupId>org.eclipse.jgit</groupId>
-        <artifactId>org.eclipse.jgit.http.server</artifactId>
-        <version>${jgitVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>junit</groupId>
-        <artifactId>junit</artifactId>
-        <version>4.11</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.h2database</groupId>
-        <artifactId>h2</artifactId>
-        <version>1.3.168</version>
-      </dependency>
-
-      <dependency>
-        <groupId>postgresql</groupId>
-        <artifactId>postgresql</artifactId>
-        <version>9.1-901-1.jdbc4</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-servlet</artifactId>
-        <version>${jettyVersion}</version>
-        <exclusions>
-          <exclusion>
-            <!-- use Apache javax.servlet not CDDL -->
-            <groupId>org.eclipse.jetty.orbit</groupId>
-            <artifactId>javax.servlet</artifactId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-
-      <dependency>
-        <groupId>org.easymock</groupId>
-        <artifactId>easymock</artifactId>
-        <version>3.0</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.apache.tomcat</groupId>
-        <artifactId>servlet-api</artifactId>
-        <version>6.0.29</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.apache.tomcat</groupId>
-        <artifactId>tomcat-servlet-api</artifactId>
-        <version>7.0.32</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.gwt</groupId>
-        <artifactId>gwt-servlet</artifactId>
-        <version>${gwtVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.gwt</groupId>
-        <artifactId>gwt-user</artifactId>
-        <version>${gwtVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.gwt</groupId>
-        <artifactId>gwt-dev</artifactId>
-        <version>${gwtVersion}</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.google.code.findbugs</groupId>
-        <artifactId>jsr305</artifactId>
-        <version>1.3.9</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.googlecode.juniversalchardet</groupId>
-        <artifactId>juniversalchardet</artifactId>
-        <version>1.0.3</version>
-      </dependency>
-
-      <dependency>
-        <groupId>dk.brics.automaton</groupId>
-        <artifactId>automaton</artifactId>
-        <version>1.11.8</version>
-      </dependency>
-
-      <dependency>
-        <groupId>com.googlecode.prolog-cafe</groupId>
-        <artifactId>PrologCafe</artifactId>
-        <version>1.3</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.pegdown</groupId>
-        <artifactId>pegdown</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.parboiled</groupId>
-        <artifactId>parboiled-core</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>org.parboiled</groupId>
-        <artifactId>parboiled-java</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-    </dependencies>
-  </dependencyManagement>
-
-  <pluginRepositories>
-    <pluginRepository>
-      <id>gerrit-maven</id>
-      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
-    </pluginRepository>
-  </pluginRepositories>
-
-  <repositories>
-    <repository>
-      <id>gerrit-maven</id>
-      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
-    </repository>
-
-    <repository>
-      <id>jgit-repository</id>
-      <url>http://download.eclipse.org/jgit/maven</url>
-    </repository>
-  </repositories>
-</project>
diff --git a/tools/BUCK b/tools/BUCK
new file mode 100644
index 0000000..b03dcbc
--- /dev/null
+++ b/tools/BUCK
@@ -0,0 +1,34 @@
+python_binary(
+  name = 'download_file',
+  main = 'download_file.py',
+  visibility = ['PUBLIC'],
+)
+
+python_binary(
+  name = 'pack_war',
+  main = 'pack_war.py',
+  deps = [':util'],
+  visibility = ['PUBLIC'],
+)
+
+python_library(
+  name = 'util',
+  srcs = ['util.py'],
+  visibility = ['PUBLIC'],
+)
+
+def shquote(s):
+  return s.replace("'", "'\\''")
+
+def os_path():
+  from os import environ
+  return environ.get('PATH')
+
+genrule(
+  name = 'buck.properties',
+  cmd = 'echo buck=`which buck`>$OUT;' +
+    ("echo PATH=\''%s'\' >>$OUT;" % shquote(os_path())),
+  deps = [],
+  out = 'buck.properties',
+  visibility = ['PUBLIC'],
+)
diff --git a/tools/build.defs b/tools/build.defs
new file mode 100644
index 0000000..b62c850
--- /dev/null
+++ b/tools/build.defs
@@ -0,0 +1,80 @@
+# 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.
+
+# These definitions support building a runnable version of Gerrit.
+
+DOCS = ['//Documentation:html.zip']
+LIBS = [
+  '//gerrit-war:log4j-config',
+  '//gerrit-war:init',
+  '//lib:postgresql',
+  '//lib/log:impl_log4j',
+]
+PGMLIBS = ['//gerrit-pgm:pgm']
+
+def scan_plugins():
+  import os
+  deps = []
+  for n in os.listdir('plugins'):
+    if os.path.exists(os.path.join('plugins', n, 'BUCK')):
+      deps.append('//plugins/%s:%s__plugin__compile' % (n, n))
+  return deps
+
+def war(
+    name,
+    libs = [],
+    pgmlibs = [],
+    context = [],
+    visibility = []
+    ):
+  cmd = ['$(exe //tools:pack_war)', '-o', '$OUT', '--tmp', '$TMP']
+  for l in libs:
+    cmd.extend(['--lib', l])
+  for l in pgmlibs:
+    cmd.extend(['--pgmlib', l])
+
+  src = []
+  dep = []
+  if context:
+    root = get_base_path()
+    if root:
+      root = '/'.join(['..' for _ in root.split('/')]) + '/'
+    for r in context:
+      dep.append(r[:r.rindex('.')])
+      if r.startswith('//'):
+        r = root + r[2:]
+      r = r.replace(':', '/')
+      src.append(genfile(r))
+    cmd.append('$SRCS')
+
+  genrule(
+    name = name,
+    cmd = ' '.join(cmd),
+    srcs = src,
+    deps = libs + pgmlibs + dep + ['//tools:pack_war'],
+    out = name + '.war',
+    visibility = visibility,
+  )
+
+def gerrit_war(name, ui = 'ui_optdbg', context = []):
+  war(
+    name = name,
+    libs = LIBS + ['//gerrit-war:version'],
+    pgmlibs = PGMLIBS,
+    context = [
+      '//gerrit-main:main_bin.jar',
+      '//gerrit-war:webapp_assets.zip',
+      '//gerrit-gwtui:' + ui + '.zip',
+    ] + context,
+  )
diff --git a/tools/default.defs b/tools/default.defs
new file mode 100644
index 0000000..4ed5635
--- /dev/null
+++ b/tools/default.defs
@@ -0,0 +1,178 @@
+# 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.
+
+# Rule definitions loaded by default into every BUCK file.
+
+def genantlr(
+    name,
+    srcs,
+    out):
+  tmp = name + '.src.zip'
+  genrule(
+    name = name,
+    srcs = srcs,
+    cmd = '$(exe //lib/antlr:antlr-tool) -o $TMP $SRCS;' +
+      'cd $TMP;' +
+      'zip -qr $OUT .',
+    deps = ['//lib/antlr:antlr-tool'],
+    out = out,
+  )
+
+def gwt_module(
+    name,
+    srcs,
+    gwtxml = None,
+    resources = [],
+    deps = [],
+    visibility = []):
+  if gwtxml:
+    resources = resources + [gwtxml]
+  resources = resources + srcs
+  java_library(
+    name = name,
+    srcs = srcs,
+    deps = deps,
+    resources = resources,
+    visibility = visibility,
+  )
+
+def gwt_application(
+    name,
+    module_target,
+    compiler_opts = [],
+    compiler_jvm_flags = [],
+    deps = [],
+    visibility = []):
+  cmd = ['$(exe //lib/gwt:compiler)', module_target, '$TMP', '$OUT']
+  cmd += compiler_opts + ['--', '$DEPS']
+  genrule(
+    name = name,
+    cmd = ' '.join(cmd),
+    deps = [
+      '//lib/gwt:compiler',
+      '//lib/gwt:dev',
+    ] + deps,
+    out = '%s.zip' % name,
+    visibility = visibility,
+  )
+
+# Compiles a Java library with additional compile-time dependencies
+# that do not show up as transitive dependencies to java_library()
+# or java_binary() rule that depends on this library.
+def java_library2(
+    name,
+    srcs = [],
+    resources = [],
+    deps = [],
+    compile_deps = [],
+    visibility = []):
+  c = name + '__compile'
+  t = name + '__link'
+  j = 'lib__%s__output/%s.jar' % (c, c)
+  o = 'lib__%s__output/%s.jar' % (name, name)
+  java_library(
+    name = c,
+    srcs = srcs,
+    resources = resources,
+    deps = deps + compile_deps,
+    visibility = ['//tools/eclipse:classpath'],
+  )
+  # Break the dependency chain by passing the newly built
+  # JAR to consumers through a prebuilt_jar().
+  genrule(
+    name = t,
+    cmd = 'mkdir -p $(dirname $OUT);ln -s $SRCS $OUT',
+    srcs = [genfile(j)],
+    deps = [':' + c],
+    out = o,
+  )
+  prebuilt_jar(
+    name = name,
+    binary_jar = genfile(o),
+    deps = deps + [':' + t],
+    visibility = visibility,
+  )
+
+def gerrit_extension(
+    name,
+    deps = [],
+    srcs = [],
+    resources = [],
+    manifest_file = None,
+    manifest_entries = [],
+    visibility = ['PUBLIC']):
+  gerrit_plugin(
+    name = name,
+    deps = deps,
+    srcs = srcs,
+    resources = resources,
+    manifest_file = manifest_file,
+    manifest_entries = manifest_entries,
+    type = 'extension',
+    visibility = visibility,
+  )
+
+def gerrit_plugin(
+    name,
+    deps = [],
+    srcs = [],
+    resources = [],
+    manifest_file = None,
+    manifest_entries = [],
+    type = 'plugin',
+    visibility = ['PUBLIC']):
+  mf_cmd = 'v=$(git describe HEAD);'
+  if manifest_file:
+    mf_src = [manifest_file]
+    mf_cmd += 'sed "s:@VERSION@:$v:g" $SRCS >$OUT'
+  else:
+    mf_src = []
+    mf_cmd += 'echo "Manifest-Version: 1.0" >$OUT;'
+    mf_cmd += 'echo "Gerrit-ApiType: %s" >>$OUT;' % type
+    mf_cmd += 'echo "Implementation-Version: $v" >>$OUT'
+    for line in manifest_entries:
+      mf_cmd += ';echo "%s" >> $OUT' % line
+  genrule(
+    name = name + '__manifest',
+    cmd = mf_cmd,
+    srcs = mf_src,
+    out = 'MANIFEST.MF',
+  )
+  java_library2(
+    name = name + '__plugin',
+    srcs = srcs,
+    resources = resources,
+    deps = deps,
+    compile_deps = ['//:%s-lib' % type],
+  )
+  java_binary(
+    name = name,
+    manifest_file = genfile('MANIFEST.MF'),
+    deps = [
+      ':%s__plugin' % name,
+      ':%s__manifest' % name,
+    ],
+    visibility = visibility,
+  )
+
+def java_sources(
+    name,
+    srcs,
+    visibility = []
+  ):
+  java_library(
+    name = name,
+    resources = srcs,
+    visibility = visibility,
+  )
diff --git a/tools/deploy_api.sh b/tools/deploy_api.sh
deleted file mode 100755
index 7d1ff06..0000000
--- a/tools/deploy_api.sh
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/bin/sh
-
-set -e
-
-SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
-VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
-VER=${VER%-sources.jar}
-
-type=release
-case $VER in
-*-SNAPSHOT)
-  echo >&2 "fatal: Cannot deploy $VER"
-  echo >&2 "       Use ./tools/version.sh --release && mvn clean package"
-  exit 1
-  ;;
-*-[0-9]*-g*) type=snapshot ;;
-esac
-URL=gs://gerrit-api/$type
-
-
-echo "Deploying $type gerrit-extension-api $VER"
-mvn deploy:deploy-file \
-  -DgroupId=com.google.gerrit \
-  -DartifactId=gerrit-extension-api \
-  -Dversion=$VER \
-  -Dpackaging=jar \
-  -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all.jar \
-  -DrepositoryId=gerrit-api-repository \
-  -Durl=$URL
-
-mvn deploy:deploy-file \
-  -DgroupId=com.google.gerrit \
-  -DartifactId=gerrit-extension-api \
-  -Dversion=$VER \
-  -Dpackaging=java-source \
-  -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all-sources.jar \
-  -Djava-source=false \
-  -DrepositoryId=gerrit-api-repository \
-  -Durl=$URL
-
-mvn deploy:deploy-file \
-  -DgroupId=com.google.gerrit \
-  -DartifactId=gerrit-extension-api \
-  -Dversion=$VER \
-  -Dpackaging=jar \
-  -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-javadoc.jar \
-  -Djava-source=false \
-  -DrepositoryId=gerrit-api-repository \
-  -Durl=$URL
-
-
-echo "Deploying $type gerrit-plugin-api $VER"
-mvn deploy:deploy-file \
-  -DgroupId=com.google.gerrit \
-  -DartifactId=gerrit-plugin-api \
-  -Dversion=$VER \
-  -Dpackaging=jar \
-  -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER.jar \
-  -DrepositoryId=gerrit-api-repository \
-  -Durl=$URL
-
-mvn deploy:deploy-file \
-  -DgroupId=com.google.gerrit \
-  -DartifactId=gerrit-plugin-api \
-  -Dversion=$VER \
-  -Dpackaging=java-source \
-  -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER-sources.jar \
-  -Djava-source=false \
-  -DrepositoryId=gerrit-api-repository \
-  -Durl=$URL
-
-mvn deploy:deploy-file \
-  -DgroupId=com.google.gerrit \
-  -DartifactId=gerrit-plugin-api \
-  -Dversion=$VER \
-  -Dpackaging=jar \
-  -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER-javadoc.jar \
-  -Djava-source=false \
-  -DrepositoryId=gerrit-api-repository \
-  -Durl=$URL
-
diff --git a/tools/download_all.py b/tools/download_all.py
new file mode 100755
index 0000000..241d20b
--- /dev/null
+++ b/tools/download_all.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+# 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.
+
+from optparse import OptionParser
+import re
+from subprocess import check_call, CalledProcessError, Popen, PIPE
+
+MAIN = ['//tools/eclipse:classpath']
+PAT = re.compile(r'"(//.*?)" -> "//tools:download_file"')
+
+opts = OptionParser()
+opts.add_option('--src', action='store_true')
+args, _ = opts.parse_args()
+
+targets = set()
+
+p = Popen(['buck', 'audit', 'classpath', '--dot'] + MAIN, stdout = PIPE)
+for line in p.stdout:
+  m = PAT.search(line)
+  if m:
+    n = m.group(1)
+    if args.src and n.endswith('__download_bin'):
+      n = n[:-4] + '_src'
+    targets.add(n)
+r = p.wait()
+if r != 0:
+  exit(r)
+
+try:
+  check_call(['buck', 'build'] + sorted(targets))
+except CalledProcessError as err:
+  exit(1)
diff --git a/tools/download_file.py b/tools/download_file.py
new file mode 100755
index 0000000..8d76a40
--- /dev/null
+++ b/tools/download_file.py
@@ -0,0 +1,208 @@
+#!/usr/bin/python
+# 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.
+
+from __future__ import print_function
+
+from hashlib import sha1
+from optparse import OptionParser
+from os import link, makedirs, path, remove
+import shutil
+from subprocess import check_call, CalledProcessError
+from sys import stderr
+from zipfile import ZipFile, BadZipfile, LargeZipFile
+
+REPO_ROOTS = {
+  'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
+  'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
+  'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
+  'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
+}
+
+GERRIT_HOME = path.expanduser('~/.gerritcodereview')
+CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
+LOCAL_PROPERTIES = 'local.properties'
+
+
+def hashfile(p):
+  d = sha1()
+  with open(p, 'rb') as f:
+    while True:
+      b = f.read(8192)
+      if not b:
+        break
+      d.update(b)
+  return d.hexdigest()
+
+def safe_mkdirs(d):
+  if path.isdir(d):
+    return
+  try:
+    makedirs(d)
+  except OSError as err:
+    if not path.isdir(d):
+      raise err
+
+def download_properties(root_dir):
+  """ Get the download properties.
+
+  First tries to find the properties file in the given root directory,
+  and if not found there, tries in the Gerrit settings folder in the
+  user's home directory.
+
+  Returns a set of download properties, which may be empty.
+
+  """
+  p = {}
+  local_prop = path.join(root_dir, LOCAL_PROPERTIES)
+  if not path.isfile(local_prop):
+    local_prop = path.join(GERRIT_HOME, LOCAL_PROPERTIES)
+  if path.isfile(local_prop):
+    try:
+      with open(local_prop) as fd:
+        for line in fd:
+          if line.startswith('download.'):
+            d = [e.strip() for e in line.split('=', 1)]
+            name, url = d[0], d[1]
+            p[name[len('download.'):]] = url
+    except OSError:
+      pass
+  return p
+
+def cache_entry(args):
+  if args.v:
+    h = args.v
+  else:
+    h = sha1(args.u).hexdigest()
+  name = '%s-%s' % (path.basename(args.o), h)
+  return path.join(CACHE_DIR, name)
+
+def resolve_url(url, redirects):
+  s = url.find(':')
+  if s < 0:
+    return url
+  scheme, rest = url[:s], url[s+1:]
+  if scheme not in REPO_ROOTS:
+    return url
+  if scheme in redirects:
+    root = redirects[scheme]
+  else:
+    root = REPO_ROOTS[scheme]
+  root = root.rstrip('/')
+  rest = rest.lstrip('/')
+  return '/'.join([root, rest])
+
+opts = OptionParser()
+opts.add_option('-o', help='local output file')
+opts.add_option('-u', help='URL to download')
+opts.add_option('-v', help='expected content SHA-1')
+opts.add_option('-x', action='append', help='file to delete from ZIP')
+opts.add_option('--exclude_java_sources', action='store_true')
+opts.add_option('--unsign', action='store_true')
+args, _ = opts.parse_args()
+
+root_dir = args.o
+while root_dir:
+  root_dir, n = path.split(root_dir)
+  if n == 'buck-out':
+    break
+
+redirects = download_properties(root_dir)
+cache_ent = cache_entry(args)
+src_url = resolve_url(args.u, redirects)
+
+if not path.exists(cache_ent):
+  try:
+    safe_mkdirs(path.dirname(cache_ent))
+  except OSError as err:
+    print('error creating directory %s: %s' %
+          (path.dirname(cache_ent), err), file=stderr)
+    exit(1)
+
+  print('Download %s' % src_url, file=stderr)
+  try:
+    check_call(['curl', '--proxy-anyauth', '-sfo', cache_ent, src_url])
+  except OSError as err:
+    print('could not invoke curl: %s\nis curl installed?' % err, file=stderr)
+    exit(1)
+  except CalledProcessError as err:
+    print('error using curl: %s' % err, file=stderr)
+    exit(1)
+
+if args.v:
+  have = hashfile(cache_ent)
+  if args.v != have:
+    print((
+      '%s:\n' +
+      'expected %s\n' +
+      'received %s\n') % (src_url, args.v, have), file=stderr)
+    try:
+      remove(cache_ent)
+    except OSError as err:
+      if path.exists(cache_ent):
+        print('error removing %s: %s' % (cache_ent, err), file=stderr)
+    exit(1)
+
+exclude = []
+if args.x:
+  exclude += args.x
+if args.exclude_java_sources:
+  try:
+    zf = ZipFile(cache_ent, 'r')
+    try:
+      for n in zf.namelist():
+        if n.endswith('.java'):
+          exclude.append(n)
+    finally:
+      zf.close()
+  except (BadZipfile, LargeZipFile) as err:
+    print('error opening %s: %s'  % (cache_ent, err), file=stderr)
+    exit(1)
+
+if args.unsign:
+  try:
+    zf = ZipFile(cache_ent, 'r')
+    try:
+      for n in zf.namelist():
+        if (n.endswith('.RSA')
+            or n.endswith('.SF')
+            or n.endswith('.LIST')):
+          exclude.append(n)
+    finally:
+      zf.close()
+  except (BadZipfile, LargeZipFile) as err:
+    print('error opening %s: %s'  % (cache_ent, err), file=stderr)
+    exit(1)
+
+safe_mkdirs(path.dirname(args.o))
+if exclude:
+  try:
+    shutil.copyfile(cache_ent, args.o)
+  except (shutil.Error, IOError) as err:
+    print('error copying to %s: %s' % (args.o, err), file=stderr)
+    exit(1)
+  try:
+    check_call(['zip', '-d', args.o] + exclude)
+  except CalledProcessError as err:
+    print('error removing files from zip: %s' % err, file=stderr)
+    exit(1)
+else:
+  try:
+    link(cache_ent, args.o)
+  except OSError as err:
+    try:
+      shutil.copyfile(cache_ent, args.o)
+    except (shutil.Error, IOError) as err:
+      print('error copying to %s: %s' % (args.o, err), file=stderr)
+      exit(1)
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
new file mode 100644
index 0000000..9d6dd53
--- /dev/null
+++ b/tools/eclipse/BUCK
@@ -0,0 +1,17 @@
+include_defs('//tools/build.defs')
+
+java_library(
+  name = 'classpath',
+  deps = LIBS + PGMLIBS + [
+    '//gerrit-acceptance-tests:lib',
+    '//gerrit-gwtdebug:gwtdebug',
+    '//gerrit-gwtui:ui_module',
+    '//gerrit-gwtui:ui_tests',
+    '//gerrit-httpd:httpd_tests',
+    '//gerrit-main:main_lib',
+    '//gerrit-server:server__compile',
+    '//lib/asciidoctor:asciidoc_lib',
+    '//lib/asciidoctor:doc_indexer_lib',
+    '//lib/prolog:compiler_lib',
+  ] + scan_plugins(),
+)
diff --git a/tools/eclipse/buck_daemon_ui_chrome.launch b/tools/eclipse/buck_daemon_ui_chrome.launch
new file mode 100644
index 0000000..efe2623
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_chrome.launch
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_chrome"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_dbg.launch b/tools/eclipse/buck_daemon_ui_dbg.launch
new file mode 100644
index 0000000..a345f8a
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_dbg.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_dbg"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_firefox.launch b/tools/eclipse/buck_daemon_ui_firefox.launch
new file mode 100644
index 0000000..383b051
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_firefox.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_firefox"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_ie9.launch b/tools/eclipse/buck_daemon_ui_ie9.launch
new file mode 100644
index 0000000..18863e7
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_ie9.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_ie9"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_safari.launch b/tools/eclipse/buck_daemon_ui_safari.launch
new file mode 100644
index 0000000..55259a9
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_safari.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_safari"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_gwt_debug.launch b/tools/eclipse/buck_gwt_debug.launch
new file mode 100644
index 0000000..1723cbf
--- /dev/null
+++ b/tools/eclipse/buck_gwt_debug.launch
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/buck-out/gen/lib/gwt/dev/gwt-dev-2.5.0.jar"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl /&#10;-war ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui/ui_dbg__tmp/war&#10;-server com.google.gerrit.gwtdebug.GerritDebugLauncher&#10;com.google.gerrit.GerritGwtUI"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;-XX:MaxPermSize=128M&#10;-Dgwt.persistentunitcachedir=${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui/ui_dbg__tmp/unit_cache&#10;-Dgerrit.source_root=${resource_loc:/gerrit}&#10;-Dgerrit.site_path=${resource_loc:/gerrit}/../test_site&#10;-da:com.google.gwtexpui.globalkey.client.KeyCommandSet"/>
+</launchConfiguration>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
new file mode 100755
index 0000000..4707de5
--- /dev/null
+++ b/tools/eclipse/project.py
@@ -0,0 +1,162 @@
+#!/usr/bin/python
+# 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.
+#
+# TODO(sop): Remove hack after Buck supports Eclipse
+
+from __future__ import print_function
+from optparse import OptionParser
+from os import path
+from subprocess import Popen, PIPE, CalledProcessError, check_call
+from xml.dom import minidom
+import re
+import sys
+
+MAIN = ['//tools/eclipse:classpath']
+GWT = ['//gerrit-gwtui:ui_module']
+JRE = '/'.join([
+  'org.eclipse.jdt.launching.JRE_CONTAINER',
+  'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType',
+  'JavaSE-1.6',
+])
+
+ROOT = path.abspath(__file__)
+for _ in range(0, 3):
+  ROOT = path.dirname(ROOT)
+
+opts = OptionParser()
+opts.add_option('--src', action='store_true')
+args, _ = opts.parse_args()
+
+def gen_project():
+  p = path.join(ROOT, '.project')
+  with open(p, 'w') as fd:
+    print("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+  <name>gerrit</name>
+  <buildSpec>
+    <buildCommand>
+      <name>org.eclipse.jdt.core.javabuilder</name>
+    </buildCommand>
+  </buildSpec>
+  <natures>
+    <nature>org.eclipse.jdt.core.javanature</nature>
+  </natures>
+</projectDescription>\
+""", file=fd)
+
+def gen_classpath():
+  def query_classpath(targets):
+    deps = []
+    p = Popen(['buck', 'audit', 'classpath'] + targets, stdout=PIPE)
+    for line in p.stdout:
+      deps.append(line.strip())
+    s = p.wait()
+    if s != 0:
+      exit(s)
+    return deps
+
+  def make_classpath():
+    impl = minidom.getDOMImplementation()
+    return impl.createDocument(None, 'classpath', None)
+
+  def classpathentry(kind, path, src=None):
+    e = doc.createElement('classpathentry')
+    e.setAttribute('kind', kind)
+    e.setAttribute('path', path)
+    if src:
+      e.setAttribute('sourcepath', src)
+    doc.documentElement.appendChild(e)
+
+  doc = make_classpath()
+  src = set()
+  lib = set()
+  gwt_src = set()
+  gwt_lib = set()
+
+  java_library = re.compile(r'[^/]+/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$')
+  for p in query_classpath(MAIN):
+    if p.endswith('-src.jar'):
+      # gwt_module() depends on -src.jar for Java to JavaScript compiles.
+      gwt_lib.add(p)
+      continue
+
+    if p.startswith('buck-out/gen/lib/gwt/'):
+      # gwt_module() depends on huge shaded GWT JARs that import
+      # incorrect versions of classes for Gerrit. Collect into
+      # a private grouping for later use.
+      gwt_lib.add(p)
+      continue
+
+    m = java_library.match(p)
+    if m:
+      src.add(m.group(1))
+    else:
+      lib.add(p)
+
+  for p in query_classpath(GWT):
+    m = java_library.match(p)
+    if m:
+      gwt_src.add(m.group(1))
+
+  for s in sorted(src):
+    p = path.join(s, 'java')
+    if path.exists(p):
+      classpathentry('src', p)
+      continue
+
+    for env in ['main', 'test']:
+      for srctype in ['java', 'resources']:
+        p = path.join(s, 'src', env, srctype)
+        if path.exists(p):
+          classpathentry('src', p)
+
+  for libs in [lib, gwt_lib]:
+    for j in sorted(libs):
+      s = None
+      if j.endswith('.jar'):
+        s = j[:-4] + '-src.jar'
+        if not path.exists(s):
+          s = None
+      classpathentry('lib', j, s)
+
+  for s in sorted(gwt_src):
+    classpathentry('lib', path.join(ROOT, s, 'src', 'main', 'java'))
+
+  classpathentry('con', JRE)
+  classpathentry('output', 'buck-out/classes')
+
+  p = path.join(ROOT, '.classpath')
+  with open(p, 'w') as fd:
+    doc.writexml(fd, addindent='  ', newl='\n', encoding='UTF-8')
+
+try:
+  if args.src:
+    try:
+      check_call([path.join(ROOT, 'tools', 'download_all.py'), '--src'])
+    except CalledProcessError as err:
+      exit(1)
+
+  gen_project()
+  gen_classpath()
+
+  try:
+    targets = ['//tools:buck.properties'] + MAIN + GWT
+    check_call(['buck', 'build'] + targets)
+  except CalledProcessError as err:
+    exit(1)
+except KeyboardInterrupt:
+  print('Interrupted by user', file=sys.stderr)
+  exit(1)
diff --git a/tools/git.defs b/tools/git.defs
new file mode 100644
index 0000000..c58ac88
--- /dev/null
+++ b/tools/git.defs
@@ -0,0 +1,23 @@
+# 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.
+
+def git_describe():
+  import subprocess
+  cmd = ['git', 'describe', '--match', 'v[0-9].*', '--dirty']
+  p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+  v = p.communicate()[0].strip()
+  r = p.returncode
+  if r != 0:
+    raise subprocess.CalledProcessError(r, ' '.join(cmd))
+  return v
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
deleted file mode 100644
index 8a873be..0000000
--- a/tools/gwtui_dbg.launch
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
-<stringAttribute key="bad_container_name" value="/gerrit-appja"/>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit-gwtdebug"/>
-</listAttribute>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
-<listEntry value="4"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-prettify&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtui&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtdebug&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
-<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
-<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
-<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
-<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;gerrit-gwtdebug&quot; type=&quot;1&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry path=&quot;3&quot; projectName=&quot;gerrit-war&quot; type=&quot;1&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-prettify/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-patch-jgit/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-reviewdb/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-common/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER&quot; path=&quot;3&quot; type=&quot;4&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtexpui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtjsonrpc/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtorm/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtui/target/classes&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-</listAttribute>
-<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
-<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
-<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl /&#10;-war ${resource_loc:/gerrit-gwtui/target}/gwt-hosted-mode&#10;-server com.google.gerrit.gwtdebug.GerritDebugLauncher&#10;com.google.gerrit.GerritGwtUI"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-gwtdebug"/>
-<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;-da:com.google.gwtexpui.globalkey.client.KeyCommandSet&#10;&#10;-Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
-</launchConfiguration>
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
new file mode 100644
index 0000000..0a470a4
--- /dev/null
+++ b/tools/maven/BUCK
@@ -0,0 +1,24 @@
+include_defs('//VERSION')
+include_defs('//tools/maven/package.defs')
+
+TYPE = 'snapshot' if GERRIT_VERSION.endswith('-SNAPSHOT') else 'release'
+
+maven_package(
+  repository = 'gerrit-api-repository',
+  url = 's3://gerrit-api@commondatastorage.googleapis.com/%s' % TYPE,
+  version = GERRIT_VERSION,
+  jar = {
+    'gerrit-extension-api': '//:extension-api',
+    'gerrit-plugin-api': '//:plugin-api',
+  },
+  src = {
+    'gerrit-extension-api': '//:extension-api-src',
+    'gerrit-plugin-api': '//:plugin-api-src',
+  },
+)
+
+python_binary(
+  name = 'mvn',
+  main = 'mvn.py',
+  deps = ['//tools:util'],
+)
diff --git a/tools/maven/fake_pom.xml b/tools/maven/fake_pom.xml
new file mode 100644
index 0000000..d066a4a
--- /dev/null
+++ b/tools/maven/fake_pom.xml
@@ -0,0 +1,6 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.google.gerrit</groupId>
+  <artifactId>Gerrit-Code-Review-Maven</artifactId>
+  <version>1</version>
+</project>
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
new file mode 100644
index 0000000..bdf5d01
--- /dev/null
+++ b/tools/maven/mvn.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python
+# 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.
+
+from __future__ import print_function
+from optparse import OptionParser
+from os import path
+from sys import stderr
+from util import check_output
+
+opts = OptionParser()
+opts.add_option('--repository', help='maven repository id')
+opts.add_option('--url', help='maven repository url')
+opts.add_option('-o')
+opts.add_option('-a', help='action (valid actions are: install,deploy)')
+opts.add_option('-v', help='gerrit version')
+opts.add_option('-s', action='append', help='triplet of artifactId:type:path')
+
+args, ctx = opts.parse_args()
+if not args.v:
+  print('version is empty', file=stderr)
+  exit(1)
+
+common = [
+  '-DgroupId=com.google.gerrit',
+  '-Dversion=%s' % args.v,
+]
+
+self = path.dirname(path.abspath(__file__))
+mvn = ['mvn', '--file', path.join(self, 'fake_pom.xml')]
+
+if 'install' == args.a:
+  cmd = mvn + ['install:install-file'] + common
+elif 'deploy' == args.a:
+  cmd = mvn + [
+    'deploy:deploy-file',
+    '-DrepositoryId=%s' % args.repository,
+    '-Durl=%s' % args.url,
+  ] + common
+else:
+  print("unknown action -a %s" % args.a, file=stderr)
+  exit(1)
+
+for spec in args.s:
+  artifact, packaging_type, src = spec.split(':')
+  try:
+    check_output(cmd + [
+      '-DartifactId=%s' % artifact,
+      '-Dpackaging=%s' % packaging_type,
+      '-Dfile=%s' % src,
+    ])
+  except Exception as e:
+    print('%s command failed: %s' % (args.a, e), file=stderr)
+    exit(1)
+
+with open(args.o, 'w') as fd:
+  if args.repository:
+    print('Repository: %s' % args.repository, file=fd)
+  if args.url:
+    print('URL: %s' % args.url, file=fd)
+  print('Version: %s' % args.v, file=fd)
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
new file mode 100644
index 0000000..7306031
--- /dev/null
+++ b/tools/maven/package.defs
@@ -0,0 +1,45 @@
+# 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.
+
+def maven_package(
+    version,
+    repository = None,
+    url = None,
+    jar = {},
+    src = {}):
+  cmd = ['$(exe //tools/maven:mvn)', '-v', version, '-o', '$OUT']
+  dep = []
+
+  for type,d in [('jar', jar), ('java-source', src)]:
+    for a,t in d.iteritems():
+      cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
+      dep.append(t)
+
+  genrule(
+    name = 'install',
+    cmd = ' '.join(cmd + ['-a', 'install']),
+    deps = dep + ['//tools/maven:mvn'],
+    out = 'install.info',
+  )
+
+  if repository and url:
+    genrule(
+      name = 'deploy',
+      cmd = ' '.join(cmd + [
+        '-a', 'deploy',
+        '--repository', repository,
+        '--url', url]),
+      deps = dep + ['//tools/maven:mvn'],
+      out = 'deploy.info',
+    )
diff --git a/tools/pack_war.py b/tools/pack_war.py
new file mode 100755
index 0000000..6c71d81
--- /dev/null
+++ b/tools/pack_war.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python
+# 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.
+
+from __future__ import print_function
+from optparse import OptionParser
+from os import makedirs, path, symlink
+from subprocess import check_call
+import sys
+from util import check_output
+
+opts = OptionParser()
+opts.add_option('-o', help='path to write WAR to')
+opts.add_option('--lib', action='append', help='target for WEB-INF/lib')
+opts.add_option('--pgmlib', action='append', help='target for WEB-INF/pgm-lib')
+opts.add_option('--tmp', help='temporary directory')
+args, ctx = opts.parse_args()
+
+war = args.tmp
+root = war[:war.index('buck-out')]
+jars = set()
+
+def link_jars(libs, directory):
+  makedirs(directory)
+  cp = check_output(['buck', 'audit', 'classpath'] + libs)
+  for j in cp.strip().splitlines():
+    if j not in jars:
+      jars.add(j)
+      n = path.basename(j)
+      if j.startswith('buck-out/gen/gerrit-'):
+        n = j.split('/')[2] + '-' + n
+      symlink(path.join(root, j), path.join(directory, n))
+
+if args.lib:
+  link_jars(args.lib, path.join(war, 'WEB-INF', 'lib'))
+if args.pgmlib:
+  link_jars(args.pgmlib, path.join(war, 'WEB-INF', 'pgm-lib'))
+try:
+  for s in ctx:
+    check_call(['unzip', '-q', '-d', war, s])
+  check_call(['zip', '-9qr', args.o, '.'], cwd = war)
+except KeyboardInterrupt:
+  print('Interrupted by user', file=sys.stderr)
+  exit(1)
diff --git a/tools/pgm_daemon.launch b/tools/pgm_daemon.launch
deleted file mode 100644
index cedf470..0000000
--- a/tools/pgm_daemon.launch
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
-<stringAttribute key="bad_container_name" value="/gerrit-appja"/>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit-main/src/main/java/Main.java"/>
-</listAttribute>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
-<listEntry value="1"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-war&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-main&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
-<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
-<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
-<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
-<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon&#10;--console-log&#10;--show-stack-trace&#10;-d ${resource_loc:/gerrit-parent}/../test_site"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-war"/>
-<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M"/>
-</launchConfiguration>
diff --git a/tools/release.sh b/tools/release.sh
deleted file mode 100755
index b8f3156..0000000
--- a/tools/release.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-
-flags=
-
-while [ $# -gt 0 ]
-do
-	case "$1" in
-	--no-documentation|--without-documentation)
-		flags="$flags -Dgerrit.documentation.skip=true"
-		shift
-		;;
-	--no-plugins|--without-plugins)
-		flags="$flags -Dgerrit.plugins.skip=true"
-		shift
-		;;
-	--no-tests|--without-tests)
-		flags="$flags -Dgerrit.acceptance-tests.skip=true"
-		flags="$flags -Dmaven.test.skip=true"
-		shift
-		;;
-	*)
-		echo >&2 "usage: $0 [--no-documentation] [--no-plugins] [--no-tests]"
-		exit 1
-	esac
-done
-
-git update-index -q --refresh
-
-if test -n "$(git diff-index --name-only HEAD --)" \
-|| test -n "$(git ls-files --others --exclude-standard)"
-then
-	echo >&2 "error: working directory is dirty, refusing to build"
-	exit 1
-fi
-
-./tools/version.sh --release &&
-mvn clean package verify $flags
-rc=$?
-./tools/version.sh --reset
-
-if test 0 = $rc
-then
-	echo
-	echo Built Gerrit Code Review `git describe`:
-	ls gerrit-war/target/gerrit-*.war
-	echo
-fi
-exit $rc
diff --git a/tools/util.py b/tools/util.py
new file mode 100644
index 0000000..0c121e1
--- /dev/null
+++ b/tools/util.py
@@ -0,0 +1,20 @@
+# 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.
+
+try:
+  from subprocess import check_output
+except ImportError:
+  from subprocess import Popen, PIPE
+  def check_output(*cmd):
+    return Popen(*cmd, stdout=PIPE).communicate()[0]
diff --git a/tools/util/query_tester.sh b/tools/util/query_tester.sh
new file mode 100755
index 0000000..25646c2
--- /dev/null
+++ b/tools/util/query_tester.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+usage() {
+    cat <<EOF
+  query_suite run -h <host> -i <suite_dir> -o <output_dir> [-s <suite>]
+
+  Run query test suites
+
+  A query test suite must be a subdirectory of suite_dir and contain
+  either files with queries in them, or if the suite name looks like
+  "values_operator", the files must contain values for the operator
+  instead of full queries.
+
+  The query outputs for each suite will be stored in files named
+  after their suites and input file names under the output_dir.
+
+EOF
+    exit
+}
+
+e() { [ -n "$VERBOSE" ] && echo "$@" >&2 ; "$@" ; }
+
+query()  { # host query
+    local host=$1 query="$2"
+    e ssh -p 29418 "$host" gerrit query --format json "$query"
+}
+
+run_suite() { # host idir odir suite operator
+    local host=$1 idir=$2 odir=$3 suite=$4 operator=$5 qfile files
+
+    mkdir -p "$odir/$suite"
+
+    echo "$suite" | grep -q "^values_" && operator=$(echo "$suite" | sed '-es/^values_//')
+
+    if [ -z "$suite" ] ; then
+        files=($(cd "$idir" ; echo *))
+    else
+        files=($(cd "$idir" ; echo $suite/*))
+    fi
+
+    for qfile in "${files[@]}" ; do
+        if [ -d "$idir/$qfile" ] ; then
+            run_suite "$host" "$idir" "$odir" "$qfile" "$operator"
+        else
+            if [ -n "$operator" ] ; then
+                query "$host" "$operator:{$(< "$idir/$qfile")}" > "$odir/$qfile"
+            else
+                query "$host" "$(< "$idir/$qfile")" > "$odir/$qfile"
+            fi
+        fi
+    done
+}
+
+cmd=''
+while [ $# -gt 0 ] ; do
+    case "$1" in
+        -v) VERBOSE=$1 ;;
+
+        -h) shift ; HOST=$1 ;;
+        -s) shift ; SUITE=$1 ;;
+        -i) shift ; IDIR=$1 ;;
+        -o) shift ; ODIR=$1 ;;
+
+        run) cmd=$1 ;;
+
+        --exec) shift ; "$@" ; exit ;;
+
+        *) usage ;;
+    esac
+    shift
+done
+
+case "$cmd" in
+    run)
+        [ -z "$HOST" ] && usage
+        [ -z "$IDIR" ] && usage
+        [ -z "$ODIR" ] && usage
+        [ -z "$SUITE" ] && SUITE=""
+        run_suite "$HOST" "$IDIR" "$ODIR" "$SUITE" ; exit
+    ;;
+esac
diff --git a/tools/version.sh b/tools/version.sh
deleted file mode 100755
index def099d..0000000
--- a/tools/version.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/sh
-
-# Update all pom.xml with new build number
-#
-# TODO(sop) This should be converted to some sort of
-# Java based Maven plugin so its fully portable.
-#
-
-SERVER_POMS=$(git ls-files | grep pom.xml | grep -v /src/main/resources/archetype-resources/pom.xml)
-POM_FILES=$SERVER_POMS
-
-# CORE PLUGIN LIST
-PLUGINS="commit-message-length-validator replication reviewnotes"
-for p in $PLUGINS
-do
-	POM_FILES="$POM_FILES $(cd plugins/$p && git ls-files | grep pom.xml | sed s,^,plugins/$p/,)"
-done
-
-case "$1" in
---snapshot=*)
-	V=$(echo "$1" | perl -pe 's/^--snapshot=//')
-	if [ -z "$V" ]
-	then
-		echo >&2 "usage: $0 --snapshot=0.n.0"
-		exit 1
-	fi
-	case "$V" in
-	*-SNAPSHOT) : ;;
-	*) V=$V-SNAPSHOT ;;
-	esac
-	;;
-
---release)
-	V=$(git describe HEAD) || exit
-	;;
-
---reset)
-	git checkout HEAD -- $SERVER_POMS
-	for p in $PLUGINS
-	do
-		(cd plugins/$p; git checkout $(git ls-files | grep pom.xml))
-	done
-	exit $?
-	;;
-
-*)
-	echo >&2 "usage: $0 {--snapshot=2.n | --release}"
-	exit 1
-esac
-
-case "$V" in
-v*) V=$(echo "$V" | perl -pe s/^v//) ;;
-esac
-
-perl -pi.bak -e '
-	if ($ARGV ne $old_argv) {
-		$seen_version = 0;
-		$old_argv = $ARGV;
-	}
-	if (!$seen_version) {
-		$seen_version = 1 if
-		s{(<version>).*(</version>)}{${1}'"$V"'${2}};
-	}
-	' $POM_FILES
-
-for pom in $POM_FILES
-do
-	rm -f ${pom}.bak
-done
diff --git a/website/releases/index.html b/website/releases/index.html
new file mode 100644
index 0000000..4a854d6
--- /dev/null
+++ b/website/releases/index.html
@@ -0,0 +1,164 @@
+<html>
+<head>
+  <title>Gerrit Code Review - Releases</title>
+  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
+  <style>
+  #diffy_logo {
+    float: left;
+    width: 75px;
+    height: 70px;
+    margin-right: 20px;
+  }
+  #download_container table {
+    border-spacing: 0;
+  }
+  #download_container td {
+    padding-right: 5px;
+  }
+  .latest-release {
+    background-color: lightgreen;
+  }
+  .rc {
+    padding-left: 1em;
+    font-style: italic;
+  }
+  .size {
+    text-align: right;
+  }
+  </style>
+</head>
+<body>
+
+<h1>Gerrit Code Review - Releases</h1>
+<a href="http://code.google.com/p/gerrit">
+  <img id="diffy_logo" src="https://gerrit-review.googlesource.com/static/diffy1.cache.png">
+</a>
+
+<div id='download_container'>
+</div>
+
+<script>
+$.getJSON(
+'https://www.googleapis.com/storage/v1beta2/b/gerrit-releases/o?projection=noAcl&fields=items(name%2Csize)&callback=?',
+function(data) {
+  var doc = document;
+  var frg = doc.createDocumentFragment();
+  var rx = /^gerrit(?:-full)?-([0-9.]+(?:-rc[0-9]+)?)[.]war/;
+  var docs = 'https://gerrit-documentation.storage.googleapis.com/';
+  var src = 'https://gerrit.googlesource.com/gerrit/+/'
+
+  data.items.sort(function(a,b) {
+    var av = rx.exec(a.name);
+    var bv = rx.exec(b.name);
+    if (!av || !bv) {
+      return a.name > b.name ? 1 : -1;
+    }
+
+    var an = av[1].replace('-rc', '.rc').split('.')
+    var bn = bv[1].replace('-rc', '.rc').split('.')
+    while (an.length < bn.length) an.push('0');
+    while (an.length > bn.length) bn.push('0');
+    for (var i = 0; i < an.length; i++) {
+      var ai = an[i].indexOf('rc') == 0
+        ? parseInt(an[i].substring(2))
+        : 1000 + parseInt(an[i]);
+
+      var bi = bn[i].indexOf('rc') == 0
+        ? parseInt(bn[i].substring(2))
+        : 1000 + parseInt(bn[i]);
+
+      if (ai != bi) {
+        return ai > bi ? -1 : 1;
+      }
+    }
+    return 0;
+  });
+
+  var latest = false;
+  for (var i = 0; i < data.items.length; i++) {
+    var f = data.items[i];
+    var v = rx.exec(f.name);
+
+    if ('index.html' == f.name) {
+      continue;
+    }
+
+    var tr = doc.createElement('tr');
+    var td = doc.createElement('td');
+    var a = doc.createElement('a');
+    a.href = f.name;
+    if (v) {
+      a.appendChild(doc.createTextNode('Gerrit ' + v[1]));
+    } else {
+      a.appendChild(doc.createTextNode(f.name));
+    }
+    if (f.name.indexOf('-rc') > 0) {
+      td.className = 'rc';
+    } else if (!latest) {
+      latest = true;
+      tr.className='latest-release';
+    }
+    td.appendChild(a);
+    tr.appendChild(td);
+
+    td = doc.createElement('td');
+    td.className = 'size';
+    if (f.size/(1024*1024) < 1) {
+      sizeText = Math.round(f.size/1024*10)/10 + ' KiB';
+    } else {
+      sizeText = Math.round(f.size/(1024*1024)*10)/10 + ' MiB';
+    }
+    td.appendChild(doc.createTextNode(sizeText));
+    tr.appendChild(td);
+
+    td_rel = doc.createElement('td');
+    td_doc = doc.createElement('td');
+    if (v && f.name.indexOf('-rc') < 0) {
+      // Release notes link
+      a = doc.createElement('a');
+      a.href = docs + 'ReleaseNotes/ReleaseNotes-' + v[1] + '.html';
+      a.appendChild(doc.createTextNode('Release Notes'));
+      td_rel.appendChild(a);
+
+      // Documentation link
+      a = doc.createElement('a');
+      a.href = docs + 'Documentation/' + v[1] + '/index.html';
+      a.appendChild(doc.createTextNode('Documentation'));
+      td_doc.appendChild(a);
+    }
+    tr.appendChild(td_rel);
+    tr.appendChild(td_doc);
+
+    td = doc.createElement('td');
+    if (v) {
+      a = doc.createElement('a');
+      a.href = src + 'v' + v[1];
+      a.appendChild(doc.createTextNode('src'));
+      td.appendChild(a);
+    }
+    tr.appendChild(td);
+
+    frg.appendChild(tr);
+  }
+
+  var tr = doc.createElement('tr');
+  var th = doc.createElement('th');
+  th.appendChild(doc.createTextNode('File'));
+  tr.appendChild(th);
+
+  th = doc.createElement('th');
+  th.appendChild(doc.createTextNode('Size'));
+  tr.appendChild(th);
+
+  tr.appendChild(doc.createElement('th'));
+  tr.appendChild(doc.createElement('th'));
+
+  var table = doc.createElement('table');
+  table.appendChild(tr);
+  table.appendChild(frg);
+  doc.getElementById('download_container').appendChild(table);
+});
+</script>
+
+</body>
+</html>