Merge "Change default comment visibility to expand all recent comments"
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..7c3c02d
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,14 @@
+[alias]
+  api = //:api
+  download = //:download
+  download_sources = //:download_sources
+  gerrit = //:gerrit
+  eclipse = //tools/eclipse:eclipse
+  eclipse_project = //tools/eclipse:eclipse_project
+  release = //:release
+
+[buildfile]
+  includes = //lib/DEFS
+
+[java]
+  src_roots = java, resources
diff --git a/.buckversion b/.buckversion
new file mode 100644
index 0000000..dcaab35
--- /dev/null
+++ b/.buckversion
@@ -0,0 +1 @@
+c4df74bef4e101a7e5d0176831825b7946ea64a3
diff --git a/.gitignore b/.gitignore
index c87c26f..5c55d5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
 /.classpath
 /.project
-/.settings
-/.settings/org.eclipse.jdt.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
 /test_site
 /.idea
 /gerrit-parent.iml
 *.sublime-*
+/gerrit-package-plugins
+/buck-cache
+/buck-out
+/local.properties
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..32483d6
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,11 @@
+[submodule "plugins/replication"]
+	path = plugins/replication
+	url = ../plugins/replication
+
+[submodule "plugins/reviewnotes"]
+	path = plugins/reviewnotes
+	url = ../plugins/reviewnotes
+
+[submodule "plugins/commit-message-length-validator"]
+	path = plugins/commit-message-length-validator
+	url = ../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..d467528
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,59 @@
+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'])
+
+genrule(
+  name = 'api',
+  cmd = 'echo',
+  srcs = [],
+  deps = [
+    ':extension-api',
+    ':plugin-api',
+  ],
+  out = '__fake.api__',
+)
+
+java_binary(name = 'extension-api', deps = [':extension-lib'])
+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'],
+)
+
+java_binary(name = 'plugin-api', deps = [':plugin-lib'])
+java_library(
+  name = 'plugin-lib',
+  deps = [
+    '//gerrit-server:server',
+    '//gerrit-sshd:sshd',
+    '//gerrit-httpd:httpd',
+  ],
+  export_deps = True,
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'download',
+  cmd = '${//tools:download_all}',
+  srcs = [],
+  deps = ['//tools:download_all'],
+  out = '__fake.download__',
+)
+
+genrule(
+  name = 'download_sources',
+  cmd = '${//tools:download_all} --src',
+  srcs = [],
+  deps = ['//tools:download_all'],
+  out = '__fake.download__',
+)
diff --git a/Documentation/BUCK b/Documentation/BUCK
new file mode 100644
index 0000000..47e7b53
--- /dev/null
+++ b/Documentation/BUCK
@@ -0,0 +1,62 @@
+include_defs('//Documentation/asciidoc.defs')
+
+MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
+HTML = [txt[0:-4] + '.html' for txt in SRCS]
+
+genrule(
+  name = 'html',
+  cmd = 'cd $TMP;' +
+    'mkdir -p Documentation/images;' +
+    'for s in $SRCS;do ln -s $s Documentation;done;' +
+    'mv Documentation/*.{jpg,png} Documentation/images;' +
+    'rm Documentation/licenses.txt;' +
+    'ln -s $SRCDIR/licenses.txt LICENSES.txt;' +
+    'zip -qr $OUT *',
+  srcs = [genfile(d) for d in HTML] +
+    glob([
+      'images/*.jpg',
+      'images/*.png',
+    ]) + [
+    genfile('licenses.html'),
+    genfile('licenses.txt'),
+  ],
+  deps = [':' + d for d in HTML] + [
+    ':licenses.html',
+    ':licenses.txt',
+  ],
+  out = 'html.zip',
+  visibility = ['PUBLIC'],
+)
+
+genasciidoc(
+  name = 'generate_html',
+  srcs = SRCS + [genfile('licenses.txt')],
+  outs = HTML + ['licenses.html'],
+  deps = [':config', ':licenses.txt'],
+  attributes = ['toc', 'newline="\\n"'],
+  backend = 'xhtml11',
+  conf_file = genfile('asciidoc.conf'),
+)
+
+genrule(
+  name = 'licenses.txt',
+  cmd = '${:licenses} >$OUT',
+  srcs = [],
+  deps = [':licenses'] + MAIN,
+  out = 'licenses.txt',
+)
+
+genrule(
+  name = 'config',
+  cmd = 'cp $SRCS $OUT &&' +
+    'echo "[attributes]" >>$OUT &&' +
+    'echo "revision=`git describe HEAD`" >>$OUT',
+  srcs = ['asciidoc.conf'],
+  out = 'asciidoc.conf',
+)
+
+python_binary(
+  name = 'licenses',
+  main = 'licenses.py',
+)
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 9ef6e13..ec64e0b 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -15,6 +15,7 @@
 in the `system_config` table within the database, so the groups
 can be renamed after installation if desired.
 
+
 [[administrators]]
 Administrators
 ~~~~~~~~~~~~~~
@@ -33,6 +34,7 @@
 to permit administrative users to otherwise access Gerrit as any
 other normal user would, without needing two different accounts.
 
+
 [[anonymous_users]]
 Anonymous Users
 ~~~~~~~~~~~~~~~
@@ -48,12 +50,13 @@
 to grant `Read` access to this group as Gerrit requires an account
 identity for all other operations.
 
+
 [[non-interactive_users]]
 Non-Interactive Users
 ~~~~~~~~~~~~~~~~~~~~~
 
 This is an internal user group, members of this group are not expected
-to perform interactive operations on the Gerrit web frontend.
+to perform interactive operations on the Gerrit web front-end.
 
 However, sometimes such a user may need a separate thread pool in
 order to prevent it from grabbing threads from the interactive users.
@@ -63,6 +66,7 @@
 users. This ensures that the interactive users can keep working when
 resources are tight.
 
+
 [[project_owners]]
 Project Owners
 ~~~~~~~~~~~~~~
@@ -80,6 +84,7 @@
 avoid the need to initially configure access rights for
 newly created child projects.
 
+
 [[registered_users]]
 Registered Users
 ~~~~~~~~~~~~~~~~
@@ -135,6 +140,18 @@
 `Foo-admin` typically do not need to have such rights.
 
 
+[[ldap_groups]]
+LDAP Groups
+-----------
+
+LDAP groups are Account Groups that are maintained inside of your
+LDAP instance. If you are using LDAP to manage your groups they will
+not appear in the Groups list. However you can use them just like
+regular Account Groups by prefixing your group with "ldap/" in the
+Access Control for a project. For example "ldap/foo-project" will
+add the LDAP "foo-project" group to the access list.
+
+
 Project Access Control Lists
 ----------------------------
 
@@ -244,6 +261,7 @@
 |Foo Leads       |refs/heads/qa |Code-Review| -2..+2 |
 |==============================================================
 
+
 OpenID Authentication
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -253,6 +271,7 @@
 of its OpenID identities match one or more of the patterns listed
 in the `auth.trustedOpenID` list from `gerrit.config`.
 
+
 All Projects
 ~~~~~~~~~~~~
 
@@ -272,6 +291,7 @@
 `Administrators` does, as group members would be able to alter
 permissions for every managed project including global capabilities.
 
+
 Per-Project
 ~~~~~~~~~~~
 
@@ -281,23 +301,115 @@
 granting 'DENY' within a specific project to deny a group access.
 
 
-[[access_labels]]
-Review Labels
--------------
+[[references]]
+Special and magic references
+----------------------------
 
-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 reference namespaces used in git are generally two, one for branches and
+one for tags:
 
-Gerrit comes pre-configured with several default labels that can be
-granted to groups within projects, enabling functionality for that
-group's members. link:config-labels.html[Custom labels] may also be
-defined globally or on a per-project basis.
+* +refs/heads/*+
+* +refs/tags/*+
 
-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.
+However, every reference under +refs/*+ is really available, and in Gerrit this
+opportunity for giving other refs a special meaning is used.  In Gerrit they
+are sometimes used as magic/virtual references that give the push to Gerrit a
+special meaning.
+
+
+[[references_special]]
+Special references
+~~~~~~~~~~~~~~~~~~
+
+The special references have content that's either generated by Gerrit or
+contains important project configuration that Gerrit needs. When making
+changes to these references, Gerrit will take extra precautions to verify the
+contents compatibility at upload time.
+
+
+refs/changes/*
+^^^^^^^^^^^^^^
+
+Under this namespace each uploaded patch set for every change gets a static
+reference in their git.  The format is convenient but still intended to scale to
+hundreds of thousands of patch sets.  To access a given patch set you will
+need the change number and patch set number.
+
+[verse]
+'refs/changes/'<last two digits of change number>/
+  <change number>/
+  <patch set number>
+
+You can also find these static references linked on the page of each change.
+
+
+refs/meta/config
+^^^^^^^^^^^^^^^^
+
+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
+review process.
+
+
+refs/meta/dashboards/*
+^^^^^^^^^^^^^^^^^^^^^^
+
+There's a dedicated page where you can read more about
+link:user-dashboards.html[User Dashboards].
+
+
+refs/notes/review
+^^^^^^^^^^^^^^^^^
+
+Autogenerated copy of review notes for all changes in the git.  Each log entry
+on the refs/notes/review branch also references the patch set on which the
+review is made.  This functionality is provided by the review-notes plugin.
+
+
+[[references_magic]]
+Magic references
+~~~~~~~~~~~~~~~~
+
+These are references with added functionality to them compared to a regular
+git push operation.
+
+refs/for/<branch ref>
+^^^^^^^^^^^^^^^^^^^^^
+
+Most prominent is the `refs/for/<branch ref>` reference which is the reference
+upon which we build the code review intercept before submitting a commit to
+the branch it's uploaded to.
+
+Further documentation on how to push can be found on the
+link:user-upload.html#push_create[Upload changes] page.
+
+
+refs/publish/*
+^^^^^^^^^^^^^^
+
+`refs/publish/*` is an alternative name to `refs/for/*` when pushing new changes
+and patch sets.
+
+
+refs/drafts/*
+^^^^^^^^^^^^^
+
+Push to `refs/drafts/*` creates a change like push to `refs/for/*`, except the
+resulting change remains hidden from public review.  You then have the option
+of adding individual reviewers before making the change public to all.  The
+change page will have a 'Publish' button which allows you to convert individual
+draft patch sets of a change into public patch sets for review.
+
+
+[[access_categories]]
+Access Categories
+-----------------
+
+Gerrit has several permission categories that can be granted to groups
+within projects, enabling functionality for that group's members.
+
+
 
 [[category_abandon]]
 Abandon
@@ -310,6 +422,7 @@
 This also grants the permission to restore a change if the change
 can be uploaded.
 
+
 [[category_create]]
 Create reference
 ~~~~~~~~~~~~~~~~
@@ -489,7 +602,7 @@
 ~~~~~~~~~~~~~~~~~~~~
 
 The `Push Merge Commit` access right permits the user to upload merge
-commits.  It's an addon to the <<category_push,Push>> access right, and
+commits.  It's an add-on to the <<category_push,Push>> access right, and
 so it won't be sufficient with only `Push Merge Commit` granted for a
 push to happen.  Some projects wish to restrict merges to being created
 by Gerrit. By granting `Push` without `Push Merge Commit`, the only
@@ -502,6 +615,7 @@
 the intention of the `Push Merge Commit` entry is to allow direct pushes
 of merge commits.
 
+
 [[category_push_annotated]]
 Push Annotated Tag
 ~~~~~~~~~~~~~~~~~~
@@ -611,6 +725,7 @@
 can still do the rebase locally and upload the rebased commit as a new
 patch set.
 
+
 [[category_remove_reviewer]]
 Remove Reviewer
 ~~~~~~~~~~~~~~~
@@ -626,6 +741,20 @@
 reviewer list on a change.
 
 
+[[category_review_labels]]
+Review Labels
+~~~~~~~~~~~~~
+
+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.
+
+Gerrit comes pre-configured with a default 'Code-Review' label that can
+be granted to groups within projects, enabling functionality for that
+group's members. link:config-labels.html[Custom labels] may also be
+defined globally or on a per-project basis.
+
+
 [[category_submit]]
 Submit
 ~~~~~~
@@ -641,6 +770,10 @@
 above) must enable submit, and also must not block it.  See above for
 details on each label.
 
+To link:user-upload.html#auto_merge[immediately submit a change on push]
+the caller needs to have the Submit permission on `refs/for/<ref>`
+(e.g. on `refs/for/refs/heads/master`).
+
 
 [[category_view_drafts]]
 View Drafts
@@ -696,6 +829,7 @@
 general guidelines for a typical way to set up your project on a
 brand new Gerrit instance.
 
+
 [[examples_contributor]]
 Contributor
 ~~~~~~~~~~~
@@ -752,7 +886,7 @@
 CI system
 ~~~~~~~~~
 
-A typical Continous Integration system should be able to download new changes
+A typical Continuous Integration system should be able to download new changes
 to build and then leave a verdict somehow.
 
 As an example, the popular
@@ -825,7 +959,7 @@
 also have the power to configure access rights in gits assigned to them.
 
 [WARNING]
-These users should be really knowledgable about git, for instance knowing why
+These users should be really knowledgeable about git, for instance knowing why
 tags never should be removed from a server.  This role is granted potentially
 destructive access rights and cleaning up after such a mishap could be time
 consuming!
@@ -839,6 +973,7 @@
 
 * <<category_owner,`Owner`>> in the gits they mostly work with.
 
+
 [[examples_administrator]]
 Administrator
 ~~~~~~~~~~~~~
@@ -856,6 +991,7 @@
 
 * <<examples_project-owner,Project owner rights>>
 
+
 Enforcing site wide access policies
 -----------------------------------
 
@@ -880,6 +1016,7 @@
 * Project owners can manage access rights of their projects without a danger
   of violating a site wide policy
 
+
 [[block]]
 'BLOCK' access rule
 ~~~~~~~~~~~~~~~~~~~
@@ -928,6 +1065,7 @@
 different access section of the same project or in any access section in an
 inheriting project cannot override a 'BLOCK' rule.
 
+
 Examples
 ~~~~~~~~
 
@@ -957,6 +1095,7 @@
     pushTag = group Project Owners
 ====
 
+
 Let only a dedicated group vote in a special category
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -970,51 +1109,15 @@
 
 ====
   [access "refs/heads/stable*"]
-    label-Release-Proces = block -1..+1 group Anonymous Users
-    label-Release-Proces = -1..+1 group Release Engineers
+    label-Release-Process = block -1..+1 group Anonymous Users
+    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
-recieving 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>`.
-
-
-[[system_capabilities]]
-System capabilities
+[[global_capabilities]]
+Global Capabilities
 -------------------
 
-The system capabilities control actions that the administrators of
+The global capabilities control actions that the administrators of
 the server can perform which usually affect the entire
 server in some way.  The administrators may delegate these
 capabilities to trusted groups of users.
@@ -1025,6 +1128,9 @@
 keep fewer users in the administrators group, even while spreading
 much of the server administration burden out to more users.
 
+Global capabilities are assigned to groups in the access rights settings
+of the root project ("`All-Projects`").
+
 Below you find a list of capabilities available:
 
 
@@ -1066,6 +1172,7 @@
 either link:cmd-create-project.html[create new git projects via ssh]
 or via the web UI.
 
+
 [[capability_emailReviewers]]
 Email Reviewers
 ~~~~~~~~~~~~~~~
@@ -1076,6 +1183,7 @@
 emailed.  The allow rules are evaluated before deny rules, however the default
 is to allow emailing, if no explicit rule is matched.
 
+
 [[capability_flushCaches]]
 Flush Caches
 ~~~~~~~~~~~~
@@ -1095,7 +1203,7 @@
 Allow the operation of the link:cmd-kill.html[kill command over ssh].  The
 kill command ends tasks that currently occupy the Gerrit server, usually
 a replication task or a user initiated task such as an upload-pack or
-recieve-pack.
+receive-pack.
 
 
 [[capability_priority]]
@@ -1166,6 +1274,15 @@
 replication plugin is installed on the server.
 
 
+[[capability_streamEvents]]
+Stream Events
+~~~~~~~~~~~~~
+
+Allow performing streaming of Gerrit events. This capability
+allows the granted group to
+link:cmd-stream-events.html[stream Gerrit events via ssh].
+
+
 [[capability_viewCaches]]
 View Caches
 ~~~~~~~~~~~
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
new file mode 100644
index 0000000..c6df544
--- /dev/null
+++ b/Documentation/asciidoc.defs
@@ -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.
+
+def genasciidoc(
+    name,
+    srcs = [],
+    outs = [],
+    deps = [],
+    attributes = [],
+    backend = None,
+    conf_file = None,
+    visibility = []):
+  cmd = ['asciidoc', '-o', '$OUT']
+  if backend:
+    cmd.extend(['-b', backend])
+  for attribute in attributes:
+    cmd.extend(['-a', attribute])
+  if conf_file:
+    cmd.append('-f')
+  cmd.append('$SRCS')
+
+  for p in zip(srcs, outs):
+    s, o = p
+    if conf_file:
+      src_list = [conf_file, s]
+    else:
+      src_list = [s]
+    genrule(
+      name = o,
+      cmd = ' '.join(cmd),
+      srcs = src_list,
+      deps = deps,
+      out = o,
+      visibility = visibility,
+    )
+  genrule(
+    name = name,
+    cmd = '',
+    srcs = [],
+    deps = [':' + o for o in outs],
+    out = name + '__done',
+    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 16b2eb5..85b9b56 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -18,9 +18,11 @@
 
 DESCRIPTION
 -----------
-Creates a new internal only user account for batch/role access, such
-as from an automated build system or event monitoring over
-link:cmd-stream-events.html[gerrit stream-events].
+Creates a new internal-only user account.
+
+If the account is created without an email address, it may only be
+used for batch/role access, such as from an automated build system
+or event monitoring over link:cmd-stream-events.html[gerrit stream-events].
 
 If LDAP authentication is being used, the user account is created
 without checking the LDAP directory.  Consequently users can be
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index bd602c1..d2f9648 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -61,7 +61,7 @@
 ====
   $ 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 +70,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:
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 780231c..a54803b 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.
 
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-ls-user-refs.txt b/Documentation/cmd-ls-user-refs.txt
index c0e072d..25a99d1 100644
--- a/Documentation/cmd-ls-user-refs.txt
+++ b/Documentation/cmd-ls-user-refs.txt
@@ -8,7 +8,7 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit ls-user-ref'
+'ssh' -p <port> <host> 'gerrit ls-user-refs'
   [--project PROJECT> | -p <PROJECT>]
   [--user <USER> | -u <USER>]
   [--only-refs-heads]
@@ -31,11 +31,11 @@
 -------
 --project::
 -p::
-	Name of the project for which the refs should be listed.
+	Required; Name of the project for which the refs should be listed.
 
 --user::
 -u::
-	User for which the visible refs should be listed. Gerrit
+	Required; User for which the visible refs should be listed. Gerrit
 	will query the database to find matching users, so the
 	full identity/name does not need to be specified.
 
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index f28b4f5..66bd845 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -28,7 +28,9 @@
 Queries the change database and returns results describing changes
 that match the input query.  More recently updated changes appear
 before older changes, which is the same order presented in the
-web interface.
+web interface.  For each matching change, the result contains data
+for the change's latest patch set, even if the query matched on an
+older patch set (for example an older patch set's sha1 revision).
 
 A query may be limited on the number of results it returns with the
 'limit:' operator.  If no limit is supplied an internal default
@@ -41,7 +43,7 @@
 then parsed as a query. This simplifies calling conventions over
 SSH by permitting operators to appear in different arguments.
 
-Query operators may quote values using matched curly braches
+Query operators may quote values using matched curly braces
 (e.g. `reviewerin:{Developer Group}`) to sidestep issues with 2
 levels of shell quoting (caller shell invoking SSH, and the SSH
 command line parser in the server).
@@ -49,8 +51,9 @@
 OPTIONS
 -------
 --format::
-	Formatting method for the results. TEXT is the default,
-	presenting a human readable display. JSON creates one line
+	Formatting method for the results. `TEXT` is the default,
+	presenting a human readable display. `JSON` returns
+	link:json.html#change[change attributes], one line
 	per matching record, with embedded LFs escaped.
 
 --current-patch-set::
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-stream-events.txt b/Documentation/cmd-stream-events.txt
index ce23da6..6da0ef0 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -23,7 +23,9 @@
 
 ACCESS
 ------
-Any user who has configured an SSH key.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_streamEvents[the 'Stream Events' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 4899042..44de42d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -504,13 +504,13 @@
 
 cache `"changes"`::
 +
-The size determines the number of projects that will have all its changes
-cached. If the cache is set to 1024, this means all changes for up to
-1024 projects can be held in the cache.
+The size of `memoryLimit` determines the number of projects for which
+all changes will be cached. If the cache is set to 1024, this means all
+changes for up to 1024 projects can be held in the cache.
 +
-Default size is 0 (disabled). It is disabled by default due to the fact
-that change updates are not communicated between Gerrit servers.
-Hence this cache should be disabled in an multi-master/multi-slave setup.
+Default value is 0 (disabled). It is disabled by default due to the fact
+that change updates are not communicated between Gerrit servers. Hence
+this cache should be disabled in an multi-master/multi-slave setup.
 +
 The cache should be flushed whenever the database changes table is modified
 outside of gerrit.
@@ -648,8 +648,11 @@
 Maximum number of milliseconds to wait for intraline difference data
 before giving up and disabling it for a particular file pair.  This is
 a work around for an infinite loop bug in the intraline difference
-implementation.  If computation takes longer than the timeout the
-worker thread is terminated and no intraline difference is displayed.
+implementation.
++
+If computation takes longer than the timeout, the worker thread is
+terminated, an error message is shown, and no intraline difference is
+displayed for the file pair.
 +
 Values should use common unit suffixes to express their setting:
 +
@@ -741,6 +744,11 @@
   html = $1<a href=\"http://trak.example.com/$2\">$2</a>
 ----
 
+Comment links can also be specified in `project.config` and sections in
+children override those in parents. The only restriction is that to
+avoid injecting arbitrary user-supplied HTML in the page, comment links
+defined in `project.config` may only supply `link`, not `html`.
+
 [[commentlink.name.match]]commentlink.<name>.match::
 +
 A JavaScript regular expression to match positions to be replaced
@@ -776,6 +784,21 @@
 The configuration file eats double quotes, so escaping them as
 `\"` is necessary to protect them from the parser.
 
+[[commentlink.name.enabled]]commentlink.<name>.enabled::
++
+Whether the comment link is enabled. A child project may override a
+section in a parent or the site-wide config that is disabled by
+specifying `enabled = true`.
++
+Disabling sections in `gerrit.config` can be used by site administrators
+to create a library of comment links with `html` set that are not
+user-supplied and thus can be verified to be XSS-free, but are only
+enabled for a subset of projects.
++
+Note that the names and contents of disabled sections are visible even
+to anonymous users via the
+link:rest-api-projects.html#get-config[REST API].
+
 
 [[contactstore]]Section contactstore
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1026,6 +1049,13 @@
 isn't necessary as it can be constructed from the all of the
 above properties.
 
+[[database.connectionPool]]database.connectionPool::
++
+If true, use connection pooling for database connections. Otherwise, a
+new database connection is opened for each request.
++
+Default is false for MySQL, and true for other database backends.
+
 [[database.poolLimit]]database.poolLimit::
 +
 Maximum number of open database connections.  If the server needs
@@ -1037,11 +1067,17 @@
 need multiple connections.
 +
 Default is 8.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[database.poolMinIdle]]database.poolMinIdle::
 +
 Minimum number of connections to keep idle in the pool.
 Default is 4.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[database.poolMaxIdle]]database.poolMaxIdle::
 +
@@ -1049,6 +1085,9 @@
 are more idle connections, connections will be closed instead of
 being returned back to the pool.
 Default is 4.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[database.poolMaxWait]]database.poolMaxWait::
 +
@@ -1067,6 +1106,9 @@
 If a unit suffix is not specified, `milliseconds` is assumed.
 +
 Default is `30 seconds`.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[download]]Section download
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1193,6 +1235,19 @@
 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
@@ -1300,6 +1355,16 @@
 +
 Valid values are the characters '*', '(' and ')'.
 
+[[gitweb.linkDrafts]]gitweb.linkDrafts::
++
+Whether or not Gerrit should provide links to gitweb on draft patch sets.
++
+By default, Gerrit will show links to gitweb on all patch sets. If gitweb
+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."
+
 [[groups]]Section groups
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2368,6 +2433,20 @@
 +
 By default, 2 minutes.
 
+[[sshd.idleTimeout]]sshd.idleTimeout::
++
+Time in seconds after which the server automatically terminates idle
+connections (or 0 to disable closing of idle connections).  Values
+should use common unit suffixes to express their setting:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+
++
+By default, 0.
+
 [[sshd.maxConnectionsPerUser]]sshd.maxConnectionsPerUser::
 +
 Maximum number of concurrent SSH sessions that a user account
@@ -2403,6 +2482,34 @@
 +
 By default, all supported MACs are available.
 
+[[sshd.kerberosKeytab]]sshd.kerberosKeytab::
++
+Enable kerberos authentication for SSH connections.  To permit
+kerberos authentication, the server must have a host principal
+(see `sshd.kerberosPrincipal`) which is acquired from a keytab.
+This must be provisioned by the kerberos administrators, and is
+typically installed into `/etc/krb5.keytab` on host machines.
++
+The keytab must contain at least one `host/` principal, typically
+using the host's canonical name. If it does not use the
+canonical name, the `sshd.kerberosPrincipal` should be configured
+with the correct name.
++
+By default, not set and so kerberos authentication is not enabled.
+
+[[sshd.kerberosPrincipal]]sshd.kerberosPrincipal::
++
+If kerberos authentication is enabled with `sshd.kerberosKeytab`,
+instead use the given principal name instead of the default.
+If the principal does not begin with `host/` a warning message is
+printed and may prevent successful authentication.
++
+This may be useful if the host is behind an IP load balancer or
+other SSH forwarding systems, since the principal name is constructed
+by the client and must match for kerberos authentication to work.
++
+By default, `host/canonical.host.name`
+
 [[suggest]] Section suggest
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2698,7 +2805,7 @@
 +
 Other files support site customization.
 +
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
 
 GERRIT
 ------
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-labels.txt b/Documentation/config-labels.txt
index 05da468..9d7c469 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -4,9 +4,9 @@
 As part of the code review process, reviewers score each change with
 values for each label configured for the project.  The label values that
 a given user is allowed to set are defined according to the
-link:access-control.html#access_labels[access controls].  Gerrit comes
-pre-configured with the Code-Review label that can be granted to groups
-within projects, enabling functionality for that group's members.
+link:access-control.html#category_review_labels[access controls].  Gerrit
+comes pre-configured with the Code-Review label that can be granted to
+groups within projects, enabling functionality for that group's members.
 
 
 [[label_Code-Review]]
@@ -95,7 +95,7 @@
 
 ====
   [label "Verified"]
-      functionName = MaxWithBlock
+      function = MaxWithBlock
       value = -1 Fails
       value =  0 No score
       value = +1 Verified
@@ -184,9 +184,9 @@
 in the label name, e.g. `Label-Name` is abbreviated by default as `LN`.
 
 
-[[label_functionName]]
-`label.Label-Name.functionName`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+[[label_function]]
+`label.Label-Name.function`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The name of a function for evaluating multiple votes for a label.  This
 function is only applied if the default submit rule is used for a label.
@@ -248,7 +248,7 @@
 
 ====
   [label "Copyright-Check"]
-      functionName = MaxWithBlock
+      function = MaxWithBlock
       value = -1 Do not have copyright
       value =  0 No score
       value = +1 Copyright clear
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 7161c4a..0857442 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -28,33 +28,34 @@
 Apache 2 Configuration
 ----------------------
 
-To run Gerrit behind an Apache server using 'mod_proxy', enable the
+To run Gerrit behind an Apache server we cannot use 'mod_proxy'
+directly, as Gerrit relies on getting unmodified escaped forward
+slashes. Depending on the setting of 'AllowEncodedSlashes',
+'mod_proxy' would either decode encoded slashes, or encode them once
+again. Hence, we resort to using 'mod_rewrite'. To enable the
 necessary Apache2 modules:
 
 ----
-  a2enmod proxy_http
+  a2enmod rewrite
   a2enmod ssl          ; # optional, needed for HTTPS / SSL
 ----
 
-Configure an Apache VirtualHost to proxy to the Gerrit daemon,
-setting the 'ProxyPass' line to use the 'http://' URL configured
-above.  Ensure the path of ProxyPass and httpd.listenUrl match,
-or links will redirect to incorrect locations.
+Configure an Apache VirtualHost to proxy to the Gerrit daemon, setting
+the 'RewriteRule' line to use the 'http://' URL configured above.
+Ensure the path of 'RewriteRule' (the part before '$1') and
+httpd.listenUrl match, or links will redirect to incorrect locations.
+
+Note that this configuration allows to pass encoded characters to the
+virtual host, which is potentially dangerous. Be sure to read up on
+this topic and that you understand the risks.
 
 ----
 	<VirtualHost *>
 	  ServerName review.example.com
 
-	  ProxyRequests Off
-	  ProxyVia Off
-	  ProxyPreserveHost On
-
-	  <Proxy *>
-		Order deny,allow
-		Allow from all
-	  </Proxy>
-
-	  ProxyPass /r/ http://127.0.0.1:8081/r/
+	  AllowEncodedSlashes NoDecode
+	  RewriteEngine On
+	  RewriteRule ^/r/(.*) http://localhost:8081/r/$1 [NE,P]
 	</VirtualHost>
 ----
 
diff --git a/Documentation/config-headerfooter.txt b/Documentation/config-themes.txt
similarity index 87%
rename from Documentation/config-headerfooter.txt
rename to Documentation/config-themes.txt
index ae5d8f7..c102381 100644
--- a/Documentation/config-headerfooter.txt
+++ b/Documentation/config-themes.txt
@@ -1,29 +1,39 @@
-Gerrit Code Review - Site Customization
-=======================================
+Gerrit Code Review - Themes
+===========================
 
 Gerrit supports some customization of the HTML it sends to
 the browser, allowing organizations to alter the look and
 feel of the application to fit with their general scheme.
 
+Configuration can either be sitewide or per-project. Projects without a
+specified theme inherit from their parents, or from the sitewide theme
+for `All-Projects`.
+
+Sitewide themes are stored in `'$site_path'/etc`, and per-project
+themes are stored in `'$site_path'/themes/{project-name}`. Files are
+only served from a single theme directory; if you want to modify or
+extend an inherited theme, you must copy it into the appropriate
+per-project directory.
+
 HTML Header/Footer
 ------------------
 
 At startup Gerrit reads the following files (if they exist) and
 uses them to customize the HTML page it sends to clients:
 
-* `'$site_path'/etc/GerritSiteHeader.html`
+* `<theme-dir>/GerritSiteHeader.html`
 +
 HTML is inserted below the menu bar, but above any page content.
 This is a good location for an organizational logo, or links to
 other systems like bug tracking.
 
-* `'$site_path'/etc/GerritSiteFooter.html`
+* `<theme-dir>/GerritSiteFooter.html`
 +
 HTML is inserted at the bottom of the page, below all other content,
 but just above the footer rule and the "Powered by Gerrit Code
 Review (v....)" message shown at the extreme bottom.
 
-* `'$site_path'/etc/GerritSite.css`
+* `<theme-dir>/GerritSite.css`
 +
 The CSS rules are inlined into the top of the HTML page, inside
 of a `<style>` tag.  These rules can be used to support styling
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index 0465d62..c559b0e 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -33,7 +33,7 @@
 full rights on the newly created database:
 
 ----
-  $ createuser --username=postgres -S -R -D -P -E gerrit2
+  $ createuser --username=postgres -RDIElPS gerrit2
   $ createdb --username=postgres -E UTF-8 -O gerrit2 reviewdb
 ----
 
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
new file mode 100644
index 0000000..c228bb9
--- /dev/null
+++ b/Documentation/dev-buck.txt
@@ -0,0 +1,271 @@
+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
+----
+
+
+[[eclipse]]
+Eclipse Integration
+-------------------
+
+
+Generating the Eclipse Project
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create the Eclipse project by building the `eclipse` target:
+
+----
+  buck build eclipse
+----
+
+In Eclipse, choose 'Import existing project' and select the `gerrit` project
+from the current working directory.  Do not import any of the other Maven
+based projects.
+
+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 `buck build eclipse`.
+
+
+Refreshing the Classpath
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Normally `buck build eclipse` does everything necessary to generate a working Eclipse
+environment, but if the code doesn't compile and an updated classpath is needed, the
+Eclipse project can be refreshed and missing dependency JARs can be downloaded by
+building the `eclipse_project` and `download` targets:
+
+----
+  buck build eclipse_project download
+----
+
+
+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:
+
+----
+  buck build download_sources
+----
+
+
+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
+----
+
+
+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.
+
+
+[[tests]]
+Running Unit Tests
+------------------
+
+To run all tests including acceptance tests:
+
+----
+  buck test --all
+----
+
+To exclude slow tests:
+
+----
+  buck test --all -e slow
+----
+
+
+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.
+
+Force a download of dependency JARs by building the `download` target:
+
+----
+  buck build download
+----
+
+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.
+
+
+Build Process Switch Exit Criteria
+----------------------------------
+
+The switch to Buck is an experimental process. Buck will become the
+primary build for Gerrit (and link:dev-maven.html[Maven support]
+removed) only when the following conditions are met.
+
+1. Windows support.
++
+Facebook has an intern who will be working on this (summer 2013).
+
+2. Bootstrap and stable version support.
++
+From a fresh Gerrit clone on a machine without Buck (but with some
+reasonable subset of Buck's dependencies, e.g. Python 2.7), a new
+Gerrit developer should be able to set up and start building with
+Buck by running approximately one command. There should also be some
+idea of a "stable" version of Buck, even if we just tie our build
+to specific known-good SHAs. Binary distributions are another plus,
+which I believe the Buck team has planned.
+
+3. Eclipse support.
++
+Much of this is already there. The build needs to be at least as
+reliable as it is under Maven. (This is kind of a low bar, due to
+issues like Maven not handling generated Prolog source files or
+recompiling the GWT source.)
+
+4. Build without Internet access.
++
+Currently dependencies are downloaded directly from Maven Central
+and Gerrit's Google Cloud Storage bucket. In some environments
+build systems do not have direct network access. It must be possible
+for a developer to swap out the upstream Maven Central URL with an
+internal Maven mirror, or to supply all of the JARs themselves.
+
+5. Shawn's Buck fork merged upstream.
++
+Shawn has a link:https://gerrit.googlesource.com/buck/+log/github-master..master[fork of Buck]
+with some patches necessary to build Gerrit and run its unit tests.
+These patches (or their equivalents) must be in the upstream Buck tree.
+
+6. Fix all incidental issues.
++
+Things come up that don't work. Martin just ran out of file
+descriptors, which sounds like an upstream bug.
++
+There should be a consensus that new bugs like this in upstream
+Buck are not constantly being introduced.
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 07dae3a..84cb1e0 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -31,7 +31,7 @@
 the approvers.  Even if you are not familiar with Gerrit's
 internals, it would be of great help if you can download, try
 out, and comment on new features.  If it works as advertised,
-say so, and if you have the priviliges to do so, go ahead
+say so, and if you have the privileges to do so, go ahead
 and give it a +1 Verified.  If you would find the feature
 useful, say so and give it a +1 code review.
 
@@ -161,7 +161,7 @@
   * Define non static interfaces after static interfaces in your
     class.
   * Next you should define static types and members.
-  * Finally instance members, then constuctors, and then instance
+  * Finally instance members, then constructors, and then instance
     methods.
   * Some common exceptions are private helper static methods which
     might appear near the instance methods which they help.
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 2e43b96..6fc7e0c 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]
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index d49bc5b..3344d5e 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -9,16 +9,11 @@
 
 
 [[maven]]
-Maven Plugin
-------------
+Maven
+-----
 
-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].
+For details of the setup required to build with Maven in Eclipse,
+see the link:dev-maven.html#eclipse[Maven documentation].
 
 
 [[Formatting]]
@@ -32,22 +27,10 @@
 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
+link:dev-maven.html#build[Build] once on the command line and
 then follow link:dev-readme.html#init[Site Initialization] in the
 Developer Setup guide to configure a local site for testing.
 
@@ -73,16 +56,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, three additional Git
+To debug the GWT code executing in the web browser, two additional Git
 repositories need to be cloned.
 
-* https://gerrit.googlesource.com/gwtexpui
 * https://gerrit.googlesource.com/gwtjsonrpc
 * https://gerrit.googlesource.com/gwtorm
 
@@ -104,23 +87,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-maven.txt b/Documentation/dev-maven.txt
new file mode 100644
index 0000000..c5f8a7a
--- /dev/null
+++ b/Documentation/dev-maven.txt
@@ -0,0 +1,124 @@
+Gerrit Code Review - Building with Maven
+========================================
+
+Installation
+------------
+
+
+Download and install Maven from the
+link:http://maven.apache.org/download.cgi[Apache Maven Project].
+
+For detailed instructions for configuring and running Maven, please refer
+to the link:http://maven.apache.org/run-maven/index.html[Maven User Documentation].
+
+
+[[eclipse]]
+Eclipse Integration
+-------------------
+
+
+Installing 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].
+
+Importing 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.
+
+
+[[build]]
+Building on the Command Line
+----------------------------
+
+
+Gerrit Development WAR File
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+To build the WAR file from the command line:
+
+----
+  mvn clean package
+----
+
+The output executable WAR will be placed in:
+
+----
+  gerrit-war/target/gerrit-*.war
+----
+
+Running Unit Tests
+~~~~~~~~~~~~~~~~~~
+
+
+By default the build will run the unit tests.
+
+To build without tests:
+
+----
+  mvn clean package -DskipTests
+----
+
+[[tests]]
+Running the Acceptance Tests
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+Acceptance tests are run in the Maven `verify` target:
+
+----
+  mvn clean verify
+----
+
+To build the `verify` target without the acceptance tests:
+
+----
+  mvn clean verify -Dgerrit.acceptance-tests.skip=true
+----
+
+
+Documentation
+~~~~~~~~~~~~~
+
+
+By default the build will generate and include the documentation.
+
+To build without documentation:
+
+----
+  mvn clean package -Dgerrit.documentation.skip
+----
+
+
+Core Plugins
+~~~~~~~~~~~~
+
+
+By default the build will compile and include the core plugins.
+
+To build without core plugins:
+
+----
+  mvn clean package -Dgerrit.plugins.skip
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 12eb1a7..58c836a 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -1,47 +1,43 @@
 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
-databases, as it requires no external server process.
+Apache Maven or 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:
 
 ----
-  git clone https://gerrit.googlesource.com/gerrit
+  git clone --recursive https://gerrit.googlesource.com/gerrit
   cd gerrit
 ----
 
+The `--recursive` option is needed on `git clone` to ensure that
+the core plugins, which are included as git submodules, are also
+cloned.
+
 
 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 Maven
+or Buck, refer to:
 
-[[build]]
-Building
---------
+1. link:dev-maven.html#eclipse[Eclipse integration with Maven]
 
-From the command line:
+2. link:dev-buck.html#eclipse[Eclipse integration with Buck]
 
-----
-  mvn package
-----
-
-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".
@@ -80,7 +76,8 @@
 Testing
 -------
 
-[[run-acceptance-tests]]
+
+[[tests]]
 Running the Acceptance Tests
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -91,15 +88,13 @@
 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 Maven or Buck,
+please refer to:
 
-To execute the acceptance tests run:
+1. link:dev-maven.html#tests[Running integration tests with Maven]
 
-----
-  mvn clean verify -Pacceptance
-----
+2. link:dev-buck.html#tests[Running integration tests with Buck]
+
 
 Running the Daemon
 ~~~~~~~~~~~~~~~~~~
@@ -196,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 bc52d50..ffdb6ea 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -14,8 +14,7 @@
 
 * `gerrit-maven`:
 +
-Bucket to store Gerrit Subproject Artifacts (e.g. `gwtexpui`,
-`gwtjsonrpc` etc.).
+Bucket to store Gerrit Subproject Artifacts (e.g. `gwtjsonrpc` etc.).
 
 * `gerrit-plugins`:
 +
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index d429a1cc..cd7cd34 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -111,7 +111,6 @@
 
 The subprojects to be released are:
 
-* `gwtexpui`
 * `gwtjsonrpc`
 * `gwtorm`
 * `prolog-cafe`
@@ -132,107 +131,17 @@
 to the released version
 
 
-[[prepare-gerrit]]
-Prepare Gerrit
-~~~~~~~~~~~~~~
+[[build-gerrit]]
+Build Gerrit
+~~~~~~~~~~~~
 
-In all example commands it is assumed that the last release was `2.4`
-and that now the `2.5` release is prepared.
-
-
-[[prepare-war-and-plugin-api]]
-Prepare the Gerrit WAR and the Plugin API Jar
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-* link:dev-readme.html#run-acceptance-tests[Run the acceptance tests]
-
-* Create locally a `stable-2.5` branch for making the new release
-
-* Check in the Gerrit parent `pom.xml` that no `SNAPSHOT` version of a
-Subproject is referenced
-+
-If there is a dependency to a `SNAPSHOT` version,
-link:#subproject[release the subproject] first.
-
-* Create a tag for the Gerrit release
-+
-For an `RC` release:
-+
-====
- git tag -a -m "gerrit 2.5-rc0" v2.5-rc0
-====
-+
-For a final `stable` release:
-+
-====
- git tag -a -m "gerrit 2.5" v2.5
-====
-
-* Build the Gerrit WAR (without plugins) and the Plugin API Jar
+* Build the Gerrit WAR
 +
 ====
  ./tools/release.sh
 ====
-+
-[WARNING]
-========================================================================
-Make sure you are compiling the release for all browsers. Check in your
-Maven `~/.m2/settings.xml` file that no Maven profile is active that
-limits the compilation to a certain browser.
-========================================================================
 
 * Sanity check WAR
-
-
-[[prepare-core-plugins]]
-Prepare Core Plugins
-^^^^^^^^^^^^^^^^^^^^
-The core plugins to be prepared are:
-
-* `plugins/replication`
-
-For each core plugin do:
-
-* link:dev-release-subproject.html#make-snapshot[Make a snapshot and test it]
-* link:dev-release-subproject.html#prepare-release[Prepare the Release]
-
-* Update the version of the Core Plugin in
-`gerrit-package-plugins/pom.xml` to the released version
-
-[WARNING]
-========================================================================
-Updating the plugin versions in `gerrit-package-plugins/pom.xml`
-invalidates the Gerrit Release Tag which was created before.
-
-If needed delete the tag and recreate it!
-========================================================================
-
-
-[[prepare-war-with-plugins]]
-Prepare Gerrit WAR with Core Plugins
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-* Ensure that the Core Plugins listed in `gerrit-package-plugins/pom.xml`
-point to the latest release version (no dependency to `SNAPSHOT` versions)
-
-* Ensure that the release tag points to the `HEAD` commit
-
-* Include core plugins into WAR
-+
-====
- $ ./tools/version.sh --release && mvn clean package -f gerrit-package-plugins/pom.xml
- $ ./tools/version.sh --reset
-====
-
-* Find WAR that includes the core plugins at
-`gerrit-package-plugins\target\gerrit-full-v2.5.war`
-
-* Compare `gerrit-package-plugins\target\gerrit-full-v2.5.war` with
-  `gerrit-war\target\gerrit-v2.5.war`
-+
-The only difference should be the core plugins jars under
-`WEB-INF\plugins`.
-
 * Test the new Gerrit version
 
 [[publish-gerrit]]
diff --git a/Documentation/dev-rest-api.txt b/Documentation/dev-rest-api.txt
new file mode 100644
index 0000000..869ac2b
--- /dev/null
+++ b/Documentation/dev-rest-api.txt
@@ -0,0 +1,81 @@
+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/
+----
+
+
+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/index.txt b/Documentation/index.txt
index 707a7ce..e028d37 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -1,64 +1,82 @@
 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: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]
+.. 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
+.. Plugins
+.. link:dev-design.html[System Design]
+.. link:config-contact.html[User Contact Information]
+.. link:config-reverseproxy.html[Reverse Proxy]
+.. link:pgm-index.html[Server Side Administrative Tools]
+. Developer
+.. link:dev-readme.html[Developer Setup]
+.. 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:config-validation.html[Commit Validation]
+. Maintainer
+.. link:dev-release.html[Developer Release]
+.. link:dev-release-subproject.html[Developer Subproject Release]
 
-User Guide
-----------
-
-* link:http://source.android.com/submit-patches/workflow[Default Workflow]
-* link:user-search.html[Searching Changes]
-* link:cmd-index.html[Command Line Tools]
-* link:pgm-index.html[Server Programs]
-* link:user-upload.html[Uploading Changes]
-* link:user-changeid.html[Change-Id Lines]
-* link:user-signedoffby.html[Signed-off-by Lines]
-* link:access-control.html[Access Controls]
-* link:error-messages.html[Error Messages]
-* link:rest-api.html[REST API]
-* link:user-dashboards.html[Dashboards]
-* link:user-notify.html[Subscribing to Email Notifications]
-* link:user-submodules.html[Subscribing to Git Submodules]
-* link:prolog-cookbook.html[Prolog Cookbook]
-* link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
-
-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-headerfooter.html[Site Header/Footer]
-* 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]
-* link:config-validation.html[Commit Validation]
-* link:config-labels.html[Review Labels]
-
-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: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.txt b/Documentation/install.txt
index 4b14ff0..8137f53 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.
 
 
@@ -143,7 +143,7 @@
 
 * link:config-reverseproxy.html[Reverse Proxy]
 * link:config-sso.html[Single Sign-On Systems]
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
 * link:config-gitweb.html[Gitweb Integration]
 * link:config-gerrit.html[Other System Settings]
 
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 3659ed3..700b145 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -28,7 +28,8 @@
 
 url:: Canonical URL to reach this change.
 
-commitMessage:: The full commit message for the change.
+commitMessage:: The full commit message for the change's current patch
+set.
 
 createdOn:: Time in seconds since the UNIX epoch when this change
 was created.
@@ -118,7 +119,7 @@
 
 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.
 
@@ -234,9 +235,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.py b/Documentation/licenses.py
new file mode 100755
index 0000000..fb03526
--- /dev/null
+++ b/Documentation/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/licenses.txt b/Documentation/licenses.txt
index c672709..a09b88f 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -1,7 +1,7 @@
 Gerrit Code Review - Licenses
 =============================
 
-Gerrit itself is licensed under the <<apache2,Apache License 2.0>>.
+Gerrit itself is licensed under the <<Apache2_0,Apache License 2.0>>.
 Gerrit's executable distributions also include many other software
 components that are covered by additional licenses.
 
@@ -11,34 +11,33 @@
 [options="header"]
 |======================================================================
 |Included Package           | License
-|Gerrit Code Review         | <<apache2,Apache License 2.0>>
-|gwtexpui                   | <<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]
+|Gerrit Code Review         | <<Apache2_0,Apache License 2.0>>
+|gwtjsonrpc                 | <<Apache2_0,Apache License 2.0>>
+|gwtorm                     | <<Apache2_0,Apache License 2.0>>
+|Google Gson                | <<Apache2_0,Apache License 2.0>>
+|Google Web Toolkit         | <<Apache2_0,Apache License 2.0>>
+|Guice                      | <<Apache2_0,Apache License 2.0>>
+|Guava Libraries            | <<Apache2_0,Apache License 2.0>>
+|Apache Commons Codec       | <<Apache2_0,Apache License 2.0>>
+|Apache Commons DBCP        | <<Apache2_0,Apache License 2.0>>
+|Apache Commons Http Client | <<Apache2_0,Apache License 2.0>>
+|Apache Commons Lang        | <<Apache2_0,Apache License 2.0>>
+|Apache Commons Logging     | <<Apache2_0,Apache License 2.0>>
+|Apache Commons Net         | <<Apache2_0,Apache License 2.0>>
+|Apache Commons Pool        | <<Apache2_0,Apache License 2.0>>
+|Apache Log4J               | <<Apache2_0,Apache License 2.0>>
+|Apache MINA                | <<Apache2_0,Apache License 2.0>>
+|Apache Tomcat Servlet API  | <<Apache2_0,Apache License 2.0>>
+|Apache SSHD                | <<Apache2_0,Apache License 2.0>>, see also <<sshd,NOTICE>>
+|Apache Velocity            | <<Apache2_0,Apache License 2.0>>
+|Apache Xerces              | <<Apache2_0,Apache License 2.0>>
+|OpenID4Java                | <<Apache2_0,Apache License 2.0>>
+|Neko HTML                  | <<Apache2_0,Apache License 2.0>>
+|mime-util                  | <<Apache2_0,Apache License 2.0>>
+|Jetty                      | <<Apache2_0,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>>
+|Google Code Prettify       | <<Apache2_0,Apache License 2.0>>
+|JavaEWAH                   | <<Apache2_0,Apache License 2.0>>
 |JGit                       | <<jgit,New-Style BSD>>
 |JSch                       | <<sshd,New-Style BSD>>
 |PostgreSQL JDBC Driver     | <<postgresql,New-Style BSD>>
@@ -53,7 +52,7 @@
 |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>>
+|pegdown                    | <<Apache2_0,Apache License 2.0>>
 |======================================================================
 
 Cryptography Notice
@@ -95,7 +94,7 @@
 Licenses
 --------
 
-[[apache2]]
+[[Apache2_0]]
 Apache License 2.0
 ~~~~~~~~~~~~~~~~~~
 
@@ -408,7 +407,8 @@
 
 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.
+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],
@@ -416,8 +416,7 @@
 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 either the EPL or GPL version 3.0 as GPL version 2.0 is
-not compatible with Apache License 2.0.
+under the EPL.
 
 [[h2]]
 H2 Database - EPL or modified MPL
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index cbf51c6..7a1edcf 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -1,13 +1,15 @@
-Gerrit Code Review - Server Programs
-====================================
+Gerrit Code Review - Server Side Administrative Tools
+=====================================================
 
-Server side programs can be started by executing the WAR file
+Server side tools can be started by executing the WAR file
 through the Java command line.  For example:
 
-  $ java -jar gerrit.war program [options]
+  $ java -jar gerrit.war <tool> [<options>]
 
-[[programs]]Programs
---------------------
+Tool should be one of the following names:
+
+Tools
+-----
 
 link:pgm-init.html[init]::
 	Initialize a new Gerrit server installation.
@@ -28,7 +30,7 @@
 	Display the release version of Gerrit Code Review.
 
 Transition Utilities
---------------------
+~~~~~~~~~~~~~~~~~~~~
 
 link:pgm-ScanTrackingIds.html[ScanTrackingIds]::
 	Rescan all changes after configuring trackingids.
diff --git a/Documentation/project-setup.txt b/Documentation/project-setup.txt
index 244b4b8..36d3c60 100644
--- a/Documentation/project-setup.txt
+++ b/Documentation/project-setup.txt
@@ -108,9 +108,11 @@
 fast-forwarded to the change.
 
 When Gerrit tries to do a merge, by default the merge will only
-succeed if there is no path conflict. By selecting the checkbox
-`Automatically resolve conflicts` Gerrit will try do a content merge
-if a path conflict occurs.
+succeed if there is no path conflict.  A path conflict occurs when
+the same file has also been changed on the other side of the merge.
+
+If `Automatically resolve conflicts` is enabled, Gerrit will try
+to do a content merge when a path conflict occurs.
 
 
 Registering Additional Branches
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index f4769984..579eacd 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -31,6 +31,51 @@
   {
     "_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"
   }
 ----
@@ -257,6 +302,28 @@
   Location: https://profiles/avatar/john_doe.jpeg?s=20x20
 ----
 
+[[get-avatar-change-url]]
+Get Avatar Change URL
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/avatar.change.url'
+
+Retrieves the URL where the user can change the avatar image.
+
+.Request
+----
+  GET /a/accounts/self/avatar.change.url HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  https://profiles/pictures/john.doe
+----
+
 [[get-diff-preferences]]
 Get Diff Preferences
 ~~~~~~~~~~~~~~~~~~~~
@@ -369,6 +436,11 @@
 Identifier of a global capability. Valid values are all field names of
 the link:#capability-info[CapabilityInfo] entity.
 
+[[username]]
+\{username\}
+~~~~~~~~~~~~
+The user name.
+
 
 [[json-entities]]
 JSON Entities
@@ -388,8 +460,30 @@
 |`email`       |optional|
 The email address the user prefers to be contacted through. +
 Only set if detailed account information is requested.
+|`username`    |optional|The username of the user. +
+Only set if 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.
+|============================
+
 [[capability-info]]
 CapabilityInfo
 ~~~~~~~~~~~~~~
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 09c7e11..748ec186 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -710,6 +710,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
 ~~~~~~~~~~~~~
@@ -1218,6 +1307,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 +1446,32 @@
   "revision 674ac754f91e64a0efb8087e59a176484bd534d1 is not current revision"
 ----
 
+[[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...
+----
+
 [[get-submit-type]]
 Get Submit Type
 ~~~~~~~~~~~~~~~
@@ -1641,13 +1845,73 @@
   }
 ----
 
+[[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
+    }
+  }
+----
+
+[[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...
+----
+
 [[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 +1923,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 +1944,57 @@
   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",
+    "reviewed": true,
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
 
 [[ids]]
 IDs
@@ -1715,10 +2030,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 +2048,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
 -------------
@@ -1788,6 +2102,8 @@
 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.
 |===========================
 
 [[change-info]]
@@ -1883,6 +2199,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
 ~~~~~~~~~~~
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index e800d56..4785350 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"
   }
 ----
 
@@ -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"
     }
   ]
 ----
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 9aba0e9..1dc8bc8 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -408,6 +408,42 @@
   }
 ----
 
+[[get-config]]
+Get Config
+~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/config'
+
+Gets some configuration information about a project. Note that this
+config info is not simply the contents of `project.config`; it generally
+contains fields that may have been inherited from parent projects.
+
+.Request
+----
+  GET /projects/myproject/config
+----
+
+A link:#config-info[ConfigInfo] entity is returned that describes the
+project configuration. Some fields are only visible to users that have
+read access to `refs/meta/config`.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#project_config",
+    "use_contributor_agreements": false,
+    "use_content_merge": true,
+    "use_signed_off_by": false,
+    "require_change_id": true,
+    "commentlinks": {}
+  }
+----
+
 [[run-gc]]
 Run GC
 ~~~~~~
@@ -447,6 +483,263 @@
   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
+  }
+----
+
+[[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
 -------------------
@@ -675,6 +968,12 @@
 IDs
 ---
 
+[[branch-id]]
+\{branch-id\}
+~~~~~~~~~~~~~
+The name of a branch or `HEAD`. The prefix `refs/heads/` can be
+omitted.
+
 [[dashboard-id]]
 \{dashboard-id\}
 ~~~~~~~~~~~~~~~~
@@ -693,6 +992,38 @@
 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.
+|=======================
+
 [[dashboard-info]]
 DashboardInfo
 ~~~~~~~~~~~~~
@@ -896,6 +1227,39 @@
 |`size_of_packed_objects`  |Size of packed objects in bytes.
 |======================================
 
+[[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*`|
+If set, authors must complete a contributor agreement on the site
+before pushing any commits or changes to this project.
+|`use_content_merge*`|
+If set, 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*`|
+If set, each change must contain a Signed-off-by line from either the
+author or the uploader in the commit message.
+|`require_change_id*`|
+If set, require a valid link:user-changeid.html[Change-Id] footer in any
+commit uploaded for review. 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`.
+|======================================
+
 
 GERRIT
 ------
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index fbf7d89..ccc8604 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -5,6 +5,19 @@
 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-accounts.html[/accounts/]::
+  Account related REST endpoints
+link:rest-api-changes.html[/changes/]::
+  Change related REST endpoints
+link:rest-api-groups.html[/groups/]::
+  Group related REST endpoints
+link:rest-api-projects.html[/projects/]::
+  Project related REST endpoints
+
 Protocol Details
 ----------------
 
@@ -101,7 +114,7 @@
 calling user has no sufficient permissions.
 
 E.g. some REST endpoints require that the calling user has certain
-link:access-control.html#system_capabilities[global capabilities]
+link:access-control.html#global_capabilities[global capabilities]
 assigned.
 
 `403 Forbidden` is also used if `self` is used as account ID and the
@@ -145,17 +158,6 @@
 `422 Unprocessable Entity` is returned if the ID of a resource that is
 specified in the request body cannot be resolved.
 
-Endpoints
----------
-link:rest-api-accounts.html[/accounts/]::
-  Account related REST endpoints
-link:rest-api-changes.html[/changes/]::
-  Change related REST endpoints
-link:rest-api-groups.html[/groups/]::
-  Group related REST endpoints
-link:rest-api-projects.html[/projects/]::
-  Project related REST endpoints
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index 0b67205..c13faa6 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -10,10 +10,10 @@
 Gerrit can automatically associate a new version of a change back
 to its original review, even across cherry-picks and rebases.
 
-To be picked up by Gerrit, a Change-Id line must be in the bottom
-portion (last paragraph) of a commit message, and may be mixed
-together with the Signed-off-by, Acked-by, or other such footers.
-For example:
+To be picked up by Gerrit, a Change-Id line must be in the footer
+(last paragraph) of a commit message, and may be mixed
+together with link:user-signedoffby.html[Signed-off-by], Acked-by,
+or other such lines. For example:
 
 ----
   $ git log -1
@@ -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 c602d1b..3a12b76 100644
--- a/Documentation/user-dashboards.txt
+++ b/Documentation/user-dashboards.txt
@@ -89,25 +89,32 @@
 it easy to define common dashboards for every project by simply
 defining project dashboards on the All-Projects project.
 
+Token `${project}`
+~~~~~~~~~~~~~~~~~~
+
 Project dashboard queries may contain the special `${project}` token
-which will be replaced with the project name to which the dashboard is
-being applied.  This is useful for defining dashboards designed to be
-inherited.  With this token, it is possible to cause a query in a
-project dashboard to be restricted to only changes for the project in
-which an inherited dashboard is being applied by simply adding
-`project:${project}` to the query in the dashboard.
+which will be replaced with the name of the project to which the
+dashboard is being applied.  This is useful for defining dashboards
+designed to be inherited.  With this token, it is possible to cause a
+query in a project dashboard to be restricted to only changes for the
+project in which an inherited dashboard is being applied by simply
+adding `project:${project}` to the query in the dashboard.
 
-Section dashboard
-~~~~~~~~~~~~~~~~~
+The `${project}` token can also be used in the link:#dashboard.title[
+dashboard title] and in the link:#dashboard.description[dashboard
+description].
 
-dashboard.title::
+Section `dashboard`
+~~~~~~~~~~~~~~~~~~~
+
+[[dashboard.title]]dashboard.title::
 +
 The title of the dashboard.
 +
 If not specified the path of the dashboard config file is used as
 title.
 
-dashboard.description::
+[[dashboard.description]]dashboard.description::
 +
 The description of the dashboard.
 
@@ -125,8 +132,8 @@
 ----
 
 
-Section section
-~~~~~~~~~~~~~~~
+Section `section`
+~~~~~~~~~~~~~~~~~
 
 section.<name>.query::
 +
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index cfaf3e9..6aab43f 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -1,5 +1,5 @@
-Gerrit Code Review - Superprojects subscribed to submodules updates
-===================================================================
+Gerrit Code Review - Superproject subscription to submodules updates
+====================================================================
 
 Description
 -----------
@@ -25,7 +25,7 @@
 Git Submodules Overview
 -----------------------
 
-It is a git feature that allows an external repository to be
+Submodules are a git feature that allows an external repository to be
 attached inside a repository at a specific path. The objective here
 is to provide a brief overview, further details can be found
 in the official git submodule command documentation.
@@ -37,20 +37,20 @@
 'super':
 =====
 git submodule add ssh://server/a a
-====
+=====
 
 Still considering the above example, after its execution notice that
 inside the local repository 'super' the 'a' folder is considered a
 gitlink to the external repository 'a'. Also notice a file called
-.gitmodules is created (it is a config file containing the
-subscription of 'a'). To provide the sha-1 each gitlink points to in
+.gitmodules is created (it is a configuration file containing the
+subscription of 'a'). To provide the SHA-1 each gitlink points to in
 the external repository, one should use the command:
 ====
 git submodule status
 ====
 
 In the example provided, if 'a' is updated and 'super' is supposed
-to see the latest sha-1 (considering here 'a' has only the master
+to see the latest SHA-1 (considering here 'a' has only the master
 branch), one should then commit the modified gitlink for 'a' in
 the 'super' project. Actually it would not even need to be an
 external update, one could move to 'a' folder (insider 'super'),
@@ -63,11 +63,11 @@
 Defining the Submodule Branch
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-This is required because Submodule subscription is actually the
+This is required because submodule subscription is actually the
 subscription of a submodule project and one of its branches for
 a branch of a super project.
 
-Since it manages subscriptions in the branch scope, we could have
+Since Gerrit manages subscriptions in the branch scope, we could have
 a scenario having a project called 'super' having a branch 'integration'
 subscribed to a project called 'a' in branch 'integration', and also
 having the same 'super' project but in branch 'dev' subscribed to the 'a'
@@ -77,22 +77,23 @@
 the .gitmodules file to add a branch field to each submodule
 section which is supposed to be subscribed.
 
-The branch field is not filled by the git submodule command. Its value
-should indicate the branch of a submodule project that when updated
-will trigger automatic update of its registered gitlink.
+As the branch field is a Gerrit specific field it will not be filled
+automatically by the git submodule command, so one needs to edit it
+manually. Its value should indicate the branch of a submodule project
+that when updated will trigger automatic update of its registered
+gitlink.
 
-The branch value could be '.' if the submodule project branch
+The branch value could be "'.'" if the submodule project branch
 has the same name as the destination branch of the commit having
 gitlinks/.gitmodules file.
 
-The branch field of a submodule section is a custom git submodule
-feature for Gerrit use. One should always be sure to fill it in
-editing .gitmodules file after adding submodules to a super project,
-if it is the intention to make use of the Gerrit feature introduced here.
+If the intention is to make use of the Gerrit feature described
+here, one should always be sure to update the .gitmodules file after
+adding submodules to a super project.
 
-Any git submodules which are added and not have the branch field
-available in the .gitmodules file will not be subscribed by Gerrit
-to automatically update the superproject.
+If a git submodule is added but the branch field is not added to the
+.gitmodules file, Gerrit will not create a subscription for the
+submodule and there will be no automatic updates to the superproject.
 
 Detecting and Subscribing Submodules
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -118,17 +119,62 @@
 creates a new commit on branch 'dev' of 'super' updating the gitlink
 to point to the just merged commit.
 
-Canonical Web Url
-~~~~~~~~~~~~~~~~~
+Subscription Limitations
+~~~~~~~~~~~~~~~~~~~~~~~~
 
-Gerrit will automatically update only the superprojects that added
-the submodules of urls of the running server (the one described in
-the canonical web url value in Gerrit configuration file).
+Gerrit will only automatically update superprojects where the
+submodules are hosted on the same Gerrit instance as the
+superproject. Gerrit determines this by checking the hostname of the
+submodule specified in the .gitmodules file and comparing it to the
+hostname from the canonical web URL.
+
+It is currently not possible to use the submodule subscription feature
+with a canonical web URL hostname that differs from the hostname of
+the submodule. Instead relative submodules should be used.
 
 The Gerrit instance administrator group should always certify to
-provide the canonical web url value in its configuration file. Users
-should certify to use the url value of the running Gerrit instance to
-add/subscribe submodules.
+provide the canonical web URL value in its configuration file. Users
+should certify to use the correct hostname of the running Gerrit
+instance to add/subscribe submodules.
+
+Relative submodules
+~~~~~~~~~~~~~~~~~~~
+
+To enable easier usage of Gerrit mirrors and/or distribution over
+several protocols, such as plain git and HTTP(S) as well as SSH, one
+can use relative submodules. This means that instead of providing the
+entire URL to the submodule a relative path is stated in the
+.gitmodules file.
+
+Gerrit will try to match the entire project name of the submodule
+including directories. Therefore it is important to supply the full
+path name of the Gerrit project, not only relative to the super
+repository. See the following example:
+
+We have a super repository placed under a sub directory.
+
+  product/super_repository.git
+
+To this repository we wish add a submodule "deeper" into the directory
+structure.
+
+  product/framework/subcomponent.git
+
+Now we need to edit the .gitmodules to include the complete path to
+the Gerrit project. Observe that we need to use two "../" to include
+the complete Gerrit project path.
+
+  path = subcomponent.git
+  url = ../../product/framework/subcomponent.git
+  branch = master
+
+In contrast the following will not setup proper submodule
+subscription, even if the submodule will be successfully cloned by git
+from Gerrit.
+
+  path = subcomponent.git
+  url = ../framework/subcomponent.git
+  branch = master
 
 Removing Subscriptions
 ----------------------
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 55ab895..cbb152c3 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -333,6 +333,40 @@
 make undesired changes to the public repository.
 
 
+[[auto_merge]]
+Auto-Merge during Push
+~~~~~~~~~~~~~~~~~~~~~~
+
+Changes can be directly submitted on push.  This is primarily useful
+for teams that don't want to do code review but want to use Gerrit's
+submit strategies to handle contention on busy branches.  Using
+`%submit` creates a change and submits it immediately, if the caller
+has link:access-control.html#category_submit[Submit] permission on
+`refs/for/<ref>` (e.g. on `refs/for/refs/heads/master`).
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%submit
+====
+
+On auto-merge of a change neither labels nor submit rules are checked.
+If the merge fails the change stays open, but when pushing a new patch
+set the merge can be reattempted by using `%submit` again.
+
+
+[[base]]
+Selecting Merge Base
+~~~~~~~~~~~~~~~~~~~~
+
+By default new changes are opened only for new unique commits
+that have never before been seen by the Gerrit server. Clients
+may override that behavior and force new changes to be created
+by setting the merge base SHA-1 using the '%base' argument:
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%base=$(git rev-parse origin/master)
+====
+
+
 repo upload
 -----------
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.18.txt b/ReleaseNotes/ReleaseNotes-2.0.18.txt
index 8c3e44b..303690c 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.18.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.18.txt
@@ -117,7 +117,7 @@
 ---------
 * GERRIT-5    Remove PatchSetInfo from database and get it always fr...
 +
-A very, very old bug.  We no longer mirror the commit data into the SQL database, but instead pull it directly from Git when needed.  Removing duplicated data simplifies the data store model, something that is important as we shift from a SQL database to a Git backed database.
+A very, very old bug.  We no longer mirror the commit data into the SQL database, but instead pull it directly from Git when needed.  Removing duplicated data simplifies the data store model, something that is important as we shift from an SQL database to a Git backed database.
 
 * GERRIT-220  Fix infinite loop in PatchScriptBuilder
 +
@@ -314,4 +314,4 @@
 * Cleanup display of external ids in the user settings
 * Preserve negative approvals when replacing patch sets
 * Use gwtjsonrpc 1.1.1
-* gerrit 2.0.18
\ No newline at end of file
+* gerrit 2.0.18
diff --git a/ReleaseNotes/ReleaseNotes-2.0.22.txt b/ReleaseNotes/ReleaseNotes-2.0.22.txt
index d27fcff..2021dee 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.22.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.22.txt
@@ -118,7 +118,7 @@
 oddball characters like '\0' and '/'.  We really want them to
 be a restricted subset which won't cause errors when we try to
 map SSH usernames as file names in a Git repository as we try
-to move away from a SQL database.
+to move away from an SQL database.
 
 * GERRIT-282  Fix reply to comment on left side
 +
@@ -157,4 +157,4 @@
 * GERRIT-67   Wait for dependencies to submit before claiming merge ...
 * Move abandonChange to ChangeManageService
 * Remove trailing whitespace in install.txt
-* Gerrit 2.0.22
\ No newline at end of file
+* Gerrit 2.0.22
diff --git a/ReleaseNotes/ReleaseNotes-2.5.3.txt b/ReleaseNotes/ReleaseNotes-2.5.3.txt
new file mode 100644
index 0000000..1cbe85f
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.3.txt
@@ -0,0 +1,22 @@
+Release notes for Gerrit 2.5.3
+==============================
+
+Gerrit 2.5.3 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war]
+
+There are no schema changes from any member of the 2.5.x versions.
+
+However, if upgrading from anything earlier version, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Security Fixes
+--------------
+* Patch vulnerabilities in OpenID client library
++
+Installations using OpenID for authentication were vulnerable to a
+number of attacks over the network.  The openid4java client library
+was identified as the entry point.  In this release Gerrit updated to
+the latest 0.9.8 release, which patches the known attack vectors.
+
+No other changes since 2.5.2.
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
index 044125f..85f2844 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -47,6 +47,11 @@
 Global
 ^^^^^^
 
+* New Login Screens
++
+New form based HTML screens for login allow browsers to offer the
+choice to save the login data locally in the user's password store.
+
 * Rename "Groups" top-level menu to "People"
 
 * Move "Draft Comments" link next to "Drafts" link
@@ -61,12 +66,9 @@
 
 * Always show 'Working ...' message
 +
-The 'Working ...' message is now not anymore sticked to the Gerrit page
-top border so that it is now always displayed, even if the user has
-scrolled down the page and the top border is not visible.
-+
-This change make 'Working ...' message relatively positioned to
-browser top border, so that it is always visible for users.
+The 'Working ...' message is relatively positioned from the top of
+the browser, so that the message is always visible, even if the user
+has scrolled down the page.
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#suggest.from[
   suggest.from] configures a minimum number of characters before
@@ -142,6 +144,8 @@
   labels if enabled by
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html#_how_to_write_submit_rules[submit rules].
 
+* Voting on draft changes is now possible.
+
 * Recommend rebase on Path Conflict
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=1685[Issue 1685]:
@@ -423,6 +427,13 @@
 HTML thanks to Gson encoding HTML control characters using Unicode
 character escapes within JSON strings.
 
+* Apache reverse proxies must switch to mod_rewrite
++
+When Apache is used as a reverse proxy the server must be reconfigured
+to use mod_rewrite and AllowEncodedSlashes.  For updated information
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-reverseproxy.html#_apache_2_configuration[
+review the Apache 2 Configuration documentation].
+
 Project Dashboards
 ~~~~~~~~~~~~~~~~~~
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-dashboards.html#project-dashboards[
@@ -514,6 +525,12 @@
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#category_publish_drafts[publishing]
   other users' draft changes is a new permission.
 
+* Grant most permissions when creating `All-Projects`
++
+Make Gerrit more like a Git server out-of-the box by granting both
+Administrators and Project Owners permissions to review changes, submit
+them, create branches, create tags, and push directly to branches.
+
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#ldap.groupName[
   LDAP group names] are configurable, `cn` is still the default.
 
@@ -589,6 +606,11 @@
   refs/for/master%r=alice,publish
 ----
 
+* Enable content merge by default
++
+Most teams seem to expect Gerrit to manage simple merges within a
+source code file. Enable this out-of-the-box.
+
 * Added a link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#core.useRecursiveMerge[
   server-level option] to use JGit's new, experimental recursive merger.
 
@@ -660,6 +682,11 @@
   which is included as a core plugin in the Gerrit distribution and can
   be installed during site initialization.
 * A plugin extension point for avatar images was added.
+* Allow HTTP plugins to change `static` or `docs` prefixes
++
+An HTTP plugin may want more control over its URL space, but still
+delegate to the plugin servlet's magic handling for static files and
+documentation. Add JAR attributes to configure these prefixes.
 
 Prolog
 ~~~~~~
@@ -720,6 +747,14 @@
 * Labels are no longer global;
   link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-labels.html[
   projects may define their own labels], with inheritance.
+* Don't create `Verify` category by default
++
+Most project teams seem confused with the out-of-the-box experience
+needing to vote on both `Code-Review` and `Verified` categories in
+order to submit a change. Simplify the out-of-the-box workflow to only
+have `Code-Review`. When a team installs the Hudson/Jenkins integration
+or their own build system they can now trivially add the `Verified`
+category by pasting 5 lines into `project.config`.
 
 Dev
 ~~~
@@ -1483,6 +1518,15 @@
 ** Use change-url flag for ChangeId
 ** Prevent exception for empty commit
 ** Fix pylint errors
+** Call `gerrit review` instead of `gerrit approve`
+** Make the private key argument optional
+** Support alternative ssh executable, for example `plink`
+** Support custom review labels
+** Correctly handle empty patch ID
++
+If only one of the patch IDs is empty, it should not be considered
+a trivial rebase.
+
 ** Use plain python instead of python2.6
 +
 Windows installation only has python.exe
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 26ccae7..5479101 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -9,6 +9,7 @@
 [[2_5]]
 Version 2.5.x
 -------------
+* link:ReleaseNotes-2.5.3.html[2.5.3]
 * link:ReleaseNotes-2.5.2.html[2.5.2]
 * link:ReleaseNotes-2.5.1.html[2.5.1]
 * link:ReleaseNotes-2.5.html[2.5]
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/themes/diffy/etc/GerritSite.css b/contrib/themes/diffy/etc/GerritSite.css
new file mode 100644
index 0000000..d476957
--- /dev/null
+++ b/contrib/themes/diffy/etc/GerritSite.css
@@ -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.
+ */
+#gerrit_topmenu {
+  left: 60px;
+  margin-right: 60px;
+  padding-right: 10px;
+  position: relative;
+}
+
+#diffy_logo {
+  display: block !important;
+  margin-bottom: -55px;
+  padding-left: 20px;
+  position: relative;
+  top: -45px;
+  width: 60px;
+}
diff --git a/contrib/themes/diffy/etc/GerritSiteHeader.html b/contrib/themes/diffy/etc/GerritSiteHeader.html
new file mode 100644
index 0000000..89b4db5
--- /dev/null
+++ b/contrib/themes/diffy/etc/GerritSiteHeader.html
@@ -0,0 +1,5 @@
+<div>
+  <div id="diffy_logo">
+    <img width="50" height="46" src="static/logo.png"/>
+  </div>
+</div>
diff --git a/contrib/themes/diffy/static/logo.png b/contrib/themes/diffy/static/logo.png
new file mode 100644
index 0000000..989c303
--- /dev/null
+++ b/contrib/themes/diffy/static/logo.png
Binary files differ
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index 215f2b1..e16c57d 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -27,198 +27,226 @@
 # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-# This script is designed to detect when a patchset uploaded to Gerrit is
-# 'identical' (determined via git-patch-id) and reapply reviews onto the new
-# patchset from the previous patchset.
+""" This script is designed to detect when a patchset uploaded to Gerrit is
+'identical' (determined via git-patch-id) and reapply reviews onto the new
+patchset from the previous patchset.
 
-# Get usage and help info by running: ./trivial_rebase.py --help
-# Documentation is available here: https://www.codeaurora.org/xwiki/bin/QAEP/Gerrit
+Get usage and help info by running: ./trivial_rebase.py --help
+Documentation is available here: https://www.codeaurora.org/xwiki/bin/QAEP/Gerrit
+
+"""
+
+from __future__ import print_function
 
 import argparse
 import json
 import re
 import subprocess
+import sys
 
-class CheckCallError(OSError):
-  """CheckCall() returned non-0."""
-  def __init__(self, command, cwd, retcode, stdout, stderr=None):
-    OSError.__init__(self, command, cwd, retcode, stdout, stderr)
-    self.command = command
-    self.cwd = cwd
-    self.retcode = retcode
-    self.stdout = stdout
-    self.stderr = stderr
+class TrivialRebase:
+  def __init__(self):
+    usage = "%(prog)s <required options> [--server-port=PORT]"
+    parser = argparse.ArgumentParser(usage=usage)
+    parser.add_argument("--change-url", dest="changeUrl", help="Change URL")
+    parser.add_argument("--project", help="Project path in Gerrit")
+    parser.add_argument("--commit", help="Git commit-ish for this patchset")
+    parser.add_argument("--patchset", type=int, help="The patchset number")
+    parser.add_argument("--private-key-path", dest="private_key_path",
+                        help="Full path to Gerrit SSH daemon's private host key")
+    parser.add_argument("--server", default='localhost',
+                        help="Gerrit SSH server [default: %(default)s]")
+    parser.add_argument("--server-port", dest="port", default='29418',
+                        help="Port to connect to Gerrit's SSH daemon "
+                             "[default: %(default)s]")
+    parser.add_argument("--ssh", default="ssh", help="SSH executable")
+    parser.add_argument("--ssh-port-flag", dest="ssh_port_flag", default="-p", help="SSH port flag")
 
-def CheckCall(command, cwd=None):
-  """Like subprocess.check_call() but returns stdout.
+    args = parser.parse_known_args()[0]
+    if None in [args.changeUrl, args.project, args.commit, args.patchset]:
+      parser.error("Incomplete arguments")
+    try:
+      self.changeId = re.search(r'\d+$', args.changeUrl).group()
+    except AttributeError:
+      parser.error("Invalid changeId")
+    self.project = args.project
+    self.commit = args.commit
+    self.patchset = args.patchset
+    self.private_key_path = args.private_key_path
+    self.server = args.server
+    self.port = args.port
+    self.ssh = args.ssh
+    self.ssh_port_flag = args.ssh_port_flag
 
-  Works on python 2.4
-  """
-  try:
-    process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE)
-    std_out, std_err = process.communicate()
-  except OSError, e:
-    raise CheckCallError(command, cwd, e.errno, None)
-  if process.returncode:
-    raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
-  return std_out, std_err
+  class CheckCallError(OSError):
+    """CheckCall() returned non-0."""
+    def __init__(self, command, cwd, retcode, stdout, stderr=None):
+      OSError.__init__(self, command, cwd, retcode, stdout, stderr)
+      self.command = command
+      self.cwd = cwd
+      self.retcode = retcode
+      self.stdout = stdout
+      self.stderr = stderr
 
-def GsqlQuery(sql_query, server, port):
-  """Runs a gerrit gsql query and returns the result"""
-  gsql_cmd = ['ssh', '-p', port, server, 'gerrit', 'gsql', '--format',
-              'JSON', '-c', sql_query]
-  try:
-    (gsql_out, _gsql_stderr) = CheckCall(gsql_cmd)
-  except CheckCallError, e:
-    print "return code is %s" % e.retcode
-    print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr)
-    raise
+  def CheckCall(self, command, cwd=None):
+    """Like subprocess.check_call() but returns stdout.
 
-  new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
-  return new_out.split('split here\n')
+    Works on python 2.4
 
-def FindPrevRev(changeId, patchset, server, port):
-  """Finds the revision of the previous patch set on the change"""
-  sql_query = ("\"SELECT revision FROM patch_sets WHERE "
-               "change_id = %s AND patch_set_id = %s\"" % (changeId, (patchset - 1)))
-  revisions = GsqlQuery(sql_query, server, port)
+    """
+    try:
+      process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+      std_out, std_err = process.communicate()
+    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)
+    return std_out, std_err
 
-  json_dict = json.loads(revisions[0], strict=False)
-  return json_dict["columns"]["revision"]
+  def GsqlQuery(self, sql_query):
+    """Run a gerrit gsql query and return the result."""
+    gsql_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit', 'gsql',
+                '--format', 'JSON', '-c', sql_query]
+    try:
+      (gsql_out, _gsql_stderr) = self.CheckCall(gsql_cmd)
+    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
 
-def GetApprovals(changeId, patchset, server, port):
-  """Get all the approvals on a specific patch set
+    new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
+    return new_out.split('split here\n')
 
-  Returns a list of approval dicts"""
-  sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
-               "WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
-               % (changeId, (patchset - 1)))
-  gsql_out = GsqlQuery(sql_query, server, port)
-  approvals = []
-  for json_str in gsql_out:
-    data = json.loads(json_str, strict=False)
-    if data["type"] == "row":
-      approvals.append(data["columns"])
-  return approvals
+  def FindPrevRev(self):
+    """Find the revision of the previous patch set on the change."""
+    sql_query = ("\"SELECT revision FROM patch_sets WHERE "
+                 "change_id = %s AND patch_set_id = %s\"" %
+                 (self.changeId, (self.patchset - 1)))
+    revisions = self.GsqlQuery(sql_query)
 
-def GetEmailFromAcctId(account_id, server, port):
-  """Returns the preferred email address associated with the account_id"""
-  sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\""
-               % account_id)
-  email_addr = GsqlQuery(sql_query, server, port)
+    json_dict = json.loads(revisions[0], strict=False)
+    return json_dict["columns"]["revision"]
 
-  json_dict = json.loads(email_addr[0], strict=False)
-  return json_dict["columns"]["preferred_email"]
+  def GetApprovals(self):
+    """Get all the approvals on a specific patch set.
 
-def GetPatchId(revision):
-  git_show_cmd = ['git', 'show', revision]
-  patch_id_cmd = ['git', 'patch-id']
-  patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE,
-                                      stdin=subprocess.PIPE)
-  git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE)
-  return patch_id_process.communicate(git_show_process.communicate()[0])[0]
+    Returns a list of approval dicts.
 
-def SuExec(server, port, private_key, as_user, cmd):
-  suexec_cmd = ['ssh', '-l', "Gerrit Code Review", '-p', port, server, '-i',
-                private_key, 'suexec', '--as', as_user, '--', cmd]
-  CheckCall(suexec_cmd)
+    """
+    sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
+                 "WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
+                 % (self.changeId, (self.patchset - 1)))
+    gsql_out = self.GsqlQuery(sql_query)
+    approvals = []
+    for json_str in gsql_out:
+      data = json.loads(json_str, strict=False)
+      if data["type"] == "row":
+        approvals.append(data["columns"])
+    return approvals
 
-def DiffCommitMessages(commit1, commit2):
-  log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-              commit1 + '^!']
-  commit1_log = CheckCall(log_cmd1)
-  log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-              commit2 + '^!']
-  commit2_log = CheckCall(log_cmd2)
-  if commit1_log != commit2_log:
-    return True
-  return False
+  def AppendAcctApproval(self, account_id, value):
+    try:
+      newval = self.acct_approvals[account_id] + ' ' + value
+    except KeyError:
+      newval = value
+    self.acct_approvals[account_id] = newval
 
-def Main():
-  server = 'localhost'
-  usage = "%(prog)s <required options> [--server-port=PORT]"
-  parser = argparse.ArgumentParser(usage=usage)
-  parser.add_argument("--change-url", dest="changeUrl", help="Change URL")
-  parser.add_argument("--project", help="Project path in Gerrit")
-  parser.add_argument("--commit", help="Git commit-ish for this patchset")
-  parser.add_argument("--patchset", type=int, help="The patchset number")
-  parser.add_argument("--private-key-path", dest="private_key_path",
-                      help="Full path to Gerrit SSH daemon's private host key")
-  parser.add_argument("--server-port", dest="port", default='29418',
-                      help="Port to connect to Gerrit's SSH daemon "
-                           "[default: %(default)s]")
+  def GetEmailFromAcctId(self, account_id):
+    """Return the preferred email address associated with the account_id."""
+    sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\""
+                 % account_id)
+    email_addr = self.GsqlQuery(sql_query)
 
-  args = parser.parse_known_args()[0]
-  try:
-    changeId = re.search(r'\d+', args.changeUrl).group()
-  except:
-    parser.print_help()
-    exit(0)
+    json_dict = json.loads(email_addr[0], strict=False)
+    return json_dict["columns"]["preferred_email"]
 
-  if args.patchset == 1:
-    # Nothing to detect on first patchset
-    exit(0)
-  prev_revision = None
-  prev_revision = FindPrevRev(changeId, args.patchset, server, args.port)
-  if not prev_revision:
-    # Couldn't find a previous revision
-    exit(0)
-  prev_patch_id = GetPatchId(prev_revision)
-  cur_patch_id = GetPatchId(args.commit)
-  if not (prev_patch_id and cur_patch_id):
-    if not prev_patch_id:
-      print "GetPatchId failed for commit %s" % (prev_revision)
-    if not cur_patch_id:
-      print "GetPatchId failed for commit %s" % (options.commit)
-    exit(0)
-  if cur_patch_id.split()[0] != prev_patch_id.split()[0]:
-    # patch-ids don't match
-    exit(0)
-  # Patch ids match. This is a trivial rebase.
-  # In addition to patch-id we should check if the commit message changed. Most
-  # approvers would want to re-review changes when the commit message changes.
-  changed = DiffCommitMessages(prev_revision, args.commit)
-  if changed:
-    # Insert a comment into the change letting the approvers know only the
-    # commit message changed
-    comment_msg = ("\'--message=New patchset patch-id matches previous patchset"
-                   ", but commit message has changed.'")
-    comment_cmd = ['ssh', '-p', args.port, server, 'gerrit', 'approve',
-                   '--project', args.project, comment_msg, args.commit]
-    CheckCall(comment_cmd)
-    exit(0)
+  def GetPatchId(self, revision):
+    git_show_cmd = ['git', 'show', revision]
+    patch_id_cmd = ['git', 'patch-id']
+    git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE)
+    patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE,
+                                        stdin=git_show_process.stdout)
+    res = patch_id_process.communicate()[0] or '0'
+    return res.split()[0]
 
-  # Need to get all approvals on prior patch set, then suexec them onto
-  # this patchset.
-  approvals = GetApprovals(changeId, args.patchset, server, args.port)
-  gerrit_approve_msg = ("\'Automatically re-added by Gerrit trivial rebase "
-                        "detection script.\'")
-  for approval in approvals:
-    # Note: Sites with different 'copy_min_score' values in the
-    # approval_categories DB table might want different behavior here.
-    # Additional categories should also be added if desired.
-    if approval["category_id"] == "CRVW":
-      approve_category = '--code-review'
-    elif approval["category_id"] == "VRIF":
-      # Don't re-add verifies
-      #approve_category = '--verified'
-      continue
-    elif approval["category_id"] == "SUBM":
-      # We don't care about previous submit attempts
-      continue
-    else:
-      print "Unsupported category: %s" % approval
-      exit(0)
+  def SuExec(self, as_user, cmd):
+    suexec_cmd = [self.ssh, '-l', "Gerrit Code Review", self.ssh_port_flag, self.port, self.server]
+    if self.private_key_path:
+      suexec_cmd += ['-i', self.private_key_path]
+    suexec_cmd += ['suexec', '--as', as_user, '--', cmd]
+    self.CheckCall(suexec_cmd)
 
-    score = approval["value"]
-    gerrit_approve_cmd = ['gerrit', 'approve', '--project', args.project,
-                          '--message', gerrit_approve_msg, approve_category,
-                          score, args.commit]
-    email_addr = GetEmailFromAcctId(approval["account_id"], server,
-                                    args.port)
-    SuExec(server, args.port, args.private_key_path, email_addr,
-           ' '.join(gerrit_approve_cmd))
-  exit(0)
+  def DiffCommitMessages(self, prev_commit):
+    log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
+                prev_commit + '^!']
+    commit1_log = self.CheckCall(log_cmd1)
+    log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
+                self.commit + '^!']
+    commit2_log = self.CheckCall(log_cmd2)
+    if commit1_log != commit2_log:
+      return True
+    return False
+
+  def Run(self):
+    if self.patchset == 1:
+      # Nothing to detect on first patchset
+      return
+    prev_revision = self.FindPrevRev()
+    assert prev_revision, "Previous revision not found"
+    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))
+      return
+    if cur_patch_id != prev_patch_id:
+      # patch-ids don't match
+      return
+    # Patch ids match. This is a trivial rebase.
+    # In addition to patch-id we should check if the commit message changed. Most
+    # approvers would want to re-review changes when the commit message changes.
+    changed = self.DiffCommitMessages(prev_revision)
+    if changed:
+      # Insert a comment into the change letting the approvers know only the
+      # commit message changed
+      comment_msg = ("\'--message=New patchset patch-id matches previous patchset"
+                     ", but commit message has changed.'")
+      comment_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit',
+                     'review', '--project', self.project, comment_msg, self.commit]
+      self.CheckCall(comment_cmd)
+      return
+
+    # Need to get all approvals on prior patch set, then suexec them onto
+    # this patchset.
+    approvals = self.GetApprovals()
+    self.acct_approvals = dict()
+    for approval in approvals:
+      # Note: Sites with different 'copy_min_score' values in the
+      # approval_categories DB table might want different behavior here.
+      # Additional categories should also be added if desired.
+      if approval["category_id"] == "Code-Review" and approval['value'] != '-2':
+        self.AppendAcctApproval(approval['account_id'], '--code-review %s' % approval['value'])
+      elif approval["category_id"] == "Verified":
+        # Don't re-add verifies
+        # self.AppendAcctApproval(approval['account_id'], '--verified %s' % approval['value'])
+        continue
+      elif approval["category_id"] == "SUBM":
+        # We don't care about previous submit attempts
+        continue
+      else:
+        self.AppendAcctApproval(approval['account_id'], '--%s %s' %
+                                (approval['category_id'].lower().replace(' ', '-'),
+                                 approval['value']))
+
+    gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
+                          "detection script.\'")
+    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)
+      self.SuExec(email_addr, ' '.join(gerrit_review_cmd))
 
 if __name__ == "__main__":
-  Main()
+  try:
+    TrivialRebase().Run()
+  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..6b4c2bf
--- /dev/null
+++ b/gerrit-acceptance-tests/BUCK
@@ -0,0 +1,38 @@
+TEST = [
+  '//gerrit-httpd:httpd',
+  '//gerrit-sshd:sshd',
+  '//gerrit-server:server',
+]
+
+java_test(
+  name = 'acceptance_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = TEST + [
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-launcher:launcher',
+    '//gerrit-pgm:pgm',
+    '//gerrit-reviewdb:server',
+
+    '//lib:args4j',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:h2',
+    '//lib:jsch',
+    '//lib:jsr305',
+    '//lib:junit',
+    '//lib:servlet-api-3_0',
+
+    '//lib/log:impl_log4j',
+    '//lib/log:log4j',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/jgit:junit',
+    '//lib/openid:httpclient',
+    '//lib/openid:httpcore',
+  ],
+  source_under_test = TEST,
+  labels = ['slow'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-acceptance-tests/pom.xml b/gerrit-acceptance-tests/pom.xml
index 879be10..2345819 100644
--- a/gerrit-acceptance-tests/pom.xml
+++ b/gerrit-acceptance-tests/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-acceptance-tests</artifactId>
@@ -115,12 +115,20 @@
   <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>
+            <configuration>
+              <argLine>-Djava.io.tmpdir=${project.build.directory}</argLine>
+            </configuration>
             <executions>
               <execution>
                 <id>integration-test</id>
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..fab8397 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,17 +14,6 @@
 
 package com.google.gerrit.acceptance;
 
-import java.lang.reflect.Field;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.concurrent.BrokenBarrierException;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CyclicBarrier;
-import java.util.concurrent.ExecutorService;
-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;
@@ -32,15 +21,21 @@
 import com.google.inject.Injector;
 import com.google.inject.Module;
 
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
 class GerritServer {
 
   /** Returns fully started Gerrit server */
   static GerritServer start() throws Exception {
-
-    final String sitePath = initSite();
-
+    final File site = initSite();
     final CyclicBarrier serverStarted = new CyclicBarrier(2);
-
     final Daemon daemon = new Daemon(new Runnable() {
       public void run() {
         try {
@@ -56,10 +51,10 @@
     ExecutorService daemonService = Executors.newSingleThreadExecutor();
     daemonService.submit(new Callable<Void>() {
       public Void call() throws Exception {
-        int rc = daemon.main(new String[] {"-d", sitePath, "--headless" });
+        int rc = daemon.main(new String[] {"-d", site.getPath(), "--headless" });
         if (rc != 0) {
           System.out.println("Failed to start Gerrit daemon. Check "
-              + sitePath + "/logs/error_log");
+              + site.getPath() + "/logs/error_log");
           serverStarted.reset();
         }
         return null;
@@ -70,18 +65,18 @@
     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() 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"});
     if (rc != 0) {
       throw new RuntimeException("Couldn't initialize site");
     }
-    return path;
+    return tmp;
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
@@ -103,12 +98,14 @@
     return (T) f.get(obj);
   }
 
+  private File sitePath;
   private Daemon daemon;
   private ExecutorService daemonService;
   private Injector testInjector;
 
-  private GerritServer(Injector testInjector,
+  private GerritServer(File sitePath, Injector testInjector,
       Daemon daemon, ExecutorService daemonService) {
+    this.sitePath = sitePath;
     this.testInjector = testInjector;
     this.daemon = daemon;
     this.daemonService = daemonService;
@@ -124,5 +121,6 @@
     manager.stop();
     daemonService.shutdownNow();
     daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+    TempFileUtil.recursivelyDelete(sitePath);
   }
 }
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..1dad479 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.
+      throw new IOException("Refusing to clear symlink " + dir.getPath());
     }
-    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/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
index 9faf32a..1fb9bb5 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,6 +18,7 @@
 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;
@@ -74,10 +75,24 @@
 
   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 {
+    StringBuilder b = new StringBuilder();
+    b.append("gerrit create-project --empty-commit --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);
@@ -107,11 +122,17 @@
 
   public static String createCommit(Git git, PersonIdent i, String msg)
       throws GitAPIException, IOException {
-    return createCommit(git, i, msg, true);
+    return createCommit(git, i, msg, true, false);
   }
 
-  public static String createCommit(Git git, PersonIdent i, String msg,
-      boolean insertChangeId) throws GitAPIException, IOException {
+  public static void 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);
+  }
+
+  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);
@@ -119,6 +140,7 @@
     }
 
     final CommitCommand commitCmd = git.commit();
+    commitCmd.setAmend(amend);
     commitCmd.setAuthor(i);
     commitCmd.setCommitter(i);
     commitCmd.setMessage(msg);
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
index f905350..9799cc1 100644
--- 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
@@ -14,27 +14,15 @@
 
 package com.google.gerrit.acceptance.git;
 
-import static com.google.gerrit.acceptance.git.GitUtil.add;
 import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.git.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.git.GitUtil.createProject;
 import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
-import static com.google.gerrit.acceptance.git.GitUtil.pushHead;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
-import com.google.common.base.Function;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.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.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
@@ -45,37 +33,17 @@
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
 
-@RunWith(Parameterized.class)
 public class PushForReviewIT extends AbstractDaemonTest {
-
   private enum Protocol {
     SSH, HTTP
   }
 
-  @Parameters(name="{0}")
-  public static List<Object[]> getParam() {
-    List<Object[]> params = Lists.newArrayList();
-    for(Protocol p : Protocol.values()) {
-      params.add(new Object[] {p});
-    }
-    return params;
-  }
-
   @Inject
   private AccountCreator accounts;
 
@@ -86,11 +54,7 @@
   private Project.NameKey project;
   private Git git;
   private ReviewDb db;
-  private Protocol protocol;
-
-  public PushForReviewIT(Protocol p) {
-    this.protocol = p;
-  }
+  private String sshUrl;
 
   @Before
   public void setUp() throws Exception {
@@ -102,21 +66,25 @@
     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 (protocol) {
+    switch (p) {
       case SSH:
-        url = sshSession.getUrl();
+        url = sshUrl;
         break;
       case HTTP:
         url = admin.getHttpUrl();
         break;
       default:
-        throw new IllegalStateException("unexpected protocol: " + protocol);
+        throw new IllegalArgumentException("unexpected protocol: " + p);
     }
     git = cloneProject(url + "/" + project.get());
-    sshSession.close();
-
-    db = reviewDbProvider.open();
   }
 
   @After
@@ -125,185 +93,176 @@
   }
 
   @Test
-  public void testPushForMaster() throws GitAPIException, OrmException,
+  public void testPushForMaster_HTTP() throws GitAPIException, OrmException,
       IOException {
-    PushOneCommit push = new PushOneCommit();
-    String ref = "refs/for/master";
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, null);
+    testPushForMaster(Protocol.HTTP);
   }
 
   @Test
-  public void testPushForMasterWithTopic() throws GitAPIException,
+  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
-    PushOneCommit push = new PushOneCommit();
     String topic = "my/topic";
-    String ref = "refs/for/master/" + topic;
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+    PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
 
     // specify topic as option
-    push = new PushOneCommit();
-    ref = "refs/for/master%topic=" + topic;
-    r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+    r = pushTo("refs/for/master%topic=" + topic);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, topic);
   }
 
   @Test
-  public void testPushForMasterWithCc() throws GitAPIException, OrmException,
-      IOException, JSchException {
+  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");
-    PushOneCommit push = new PushOneCommit();
     String topic = "my/topic";
-    String ref = "refs/for/master/" + topic + "%cc=" + user.email;
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, 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");
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc=" + user.email
-        + ",cc=" + user2.email;
-    r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+    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";
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
-        + nonExistingEmail + ",cc=" + user.email;
-    r = push.to(ref);
-    assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+    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,
+  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");
-    PushOneCommit push = new PushOneCommit();
     String topic = "my/topic";
-    String ref = "refs/for/master/" + topic + "%r=" + user.email;
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
-        topic, user);
+    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");
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
-        + ",r=" + user2.email;
-    r = push.to(ref);
-    assertOkStatus(r, ref);
+    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
-    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
-        topic, user, user2);
+    r.assertChange(Change.Status.NEW, topic, user, user2);
 
     // add non-existing user as reviewer
     String nonExistingEmail = "non.existing@example.com";
-    push = new PushOneCommit();
-    ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r="
-        + nonExistingEmail + ",r=" + user.email;
-    r = push.to(ref);
-    assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+    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 {
+  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 push = new PushOneCommit();
-    String ref = "refs/drafts/master";
-    PushResult r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
+    PushOneCommit.Result r = pushTo("refs/drafts/master");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.DRAFT, null);
 
     // create draft by using 'draft' option
-    push = new PushOneCommit();
-    ref = "refs/for/master%draft";
-    r = push.to(ref);
-    assertOkStatus(r, ref);
-    assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
+    r = pushTo("refs/for/master%draft");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.DRAFT, null);
   }
 
   @Test
-  public void testPushForNonExistingBranch() throws GitAPIException,
+  public void testPushForNonExistingBranch_HTTP() throws GitAPIException,
       OrmException, IOException {
-    PushOneCommit push = new PushOneCommit();
+    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";
-    String ref = "refs/for/" + branchName;
-    PushResult r = push.to(ref);
-    assertErrorStatus(r, "branch " + branchName + " not found", ref);
+    PushOneCommit.Result r = pushTo("refs/for/" + branchName);
+    r.assertErrorStatus("branch " + branchName + " not found");
   }
 
-  private void assertChange(String changeId, Change.Status expectedStatus,
-      String expectedSubject, String expectedTopic,
-      TestAccount... expectedReviewers) throws OrmException {
-    Change c =
-        Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
-    assertEquals(expectedSubject, c.getSubject());
-    assertEquals(expectedStatus, c.getStatus());
-    assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
-    assertReviewers(c, expectedReviewers);
-  }
-
-  private void assertReviewers(Change c, TestAccount... expectedReviewers)
-      throws OrmException {
-    Set<Account.Id> expectedReviewerIds =
-        Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
-            new Function<TestAccount, Account.Id>() {
-              @Override
-              public Account.Id apply(TestAccount a) {
-                return a.id;
-              }
-            }));
-
-    for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(
-        c.currentPatchSetId())) {
-      assertTrue("unexpected reviewer " + psa.getAccountId(),
-          expectedReviewerIds.remove(psa.getAccountId()));
-    }
-    assertTrue("missing reviewers: " + expectedReviewerIds,
-        expectedReviewerIds.isEmpty());
-  }
-
-  private static void assertOkStatus(PushResult result, String ref) {
-    assertStatus(Status.OK, null, result, ref);
-  }
-
-  private static void assertErrorStatus(PushResult result,
-      String expectedMessage, String ref) {
-    assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage, result, ref);
-  }
-
-  private static void assertStatus(Status expectedStatus,
-      String expectedMessage, PushResult result, String ref) {
-    RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
-    assertEquals(refUpdate.getMessage() + "\n" + result.getMessages(),
-        expectedStatus, refUpdate.getStatus());
-    assertEquals(expectedMessage, refUpdate.getMessage());
-  }
-
-  private class PushOneCommit {
-    final static String FILE_NAME = "a.txt";
-    final static String FILE_CONTENT = "some content";
-    final static String SUBJECT = "test commit";
-    String changeId;
-
-    public PushResult to(String ref) throws GitAPIException, IOException {
-      add(git, FILE_NAME, FILE_CONTENT);
-      changeId = createCommit(git, admin.getIdent(), SUBJECT);
-      return pushHead(git, ref);
-    }
+  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
new file mode 100644
index 0000000..fb1b592
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
@@ -0,0 +1,179 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.add;
+import static com.google.gerrit.acceptance.git.GitUtil.amendCommit;
+import static com.google.gerrit.acceptance.git.GitUtil.createCommit;
+import static com.google.gerrit.acceptance.git.GitUtil.pushHead;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
+public class PushOneCommit {
+  public final static String SUBJECT = "test commit";
+
+  private final static String FILE_NAME = "a.txt";
+  private final static String FILE_CONTENT = "some content";
+
+  private final ReviewDb db;
+  private final PersonIdent i;
+
+  private final String subject;
+  private final String fileName;
+  private final String content;
+  private String changeId;
+
+  public PushOneCommit(ReviewDb db, PersonIdent i) {
+    this(db, i, SUBJECT, FILE_NAME, FILE_CONTENT);
+  }
+
+  public PushOneCommit(ReviewDb db, PersonIdent i, String subject,
+      String fileName, String content) {
+    this(db, i, subject, fileName, content, null);
+  }
+
+  public PushOneCommit(ReviewDb db, PersonIdent i, String subject,
+      String fileName, String content, String changeId) {
+    this.db = db;
+    this.i = i;
+    this.subject = subject;
+    this.fileName = fileName;
+    this.content = content;
+    this.changeId = changeId;
+  }
+
+  public Result to(Git git, String ref)
+      throws GitAPIException, IOException {
+    add(git, fileName, content);
+    if (changeId != null) {
+      amendCommit(git, i, subject, changeId);
+    } else {
+      changeId = createCommit(git, i, subject);
+    }
+    return new Result(db, ref, pushHead(git, ref), changeId, subject);
+  }
+
+  public static class Result {
+    private final ReviewDb db;
+    private final String ref;
+    private final PushResult result;
+    private final String changeId;
+    private final String subject;
+
+    private Result(ReviewDb db, String ref, PushResult result, String changeId,
+        String subject) {
+      this.db = db;
+      this.ref = ref;
+      this.result = result;
+      this.changeId = changeId;
+      this.subject = subject;
+    }
+
+    public PatchSet.Id getPatchSetId() throws OrmException {
+      return Iterables.getOnlyElement(
+          db.changes().byKey(new Change.Key(changeId))).currentPatchSetId();
+    }
+
+    public String getChangeId() {
+      return changeId;
+    }
+
+    public void assertChange(Change.Status expectedStatus,
+        String expectedTopic, TestAccount... expectedReviewers)
+        throws OrmException {
+      Change c =
+          Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
+      assertEquals(subject, c.getSubject());
+      assertEquals(expectedStatus, c.getStatus());
+      assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
+      assertReviewers(c, expectedReviewers);
+    }
+
+    private void assertReviewers(Change c, TestAccount... expectedReviewers)
+        throws OrmException {
+      Set<Account.Id> expectedReviewerIds =
+          Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
+              new Function<TestAccount, Account.Id>() {
+                @Override
+                public Account.Id apply(TestAccount a) {
+                  return a.id;
+                }
+              }));
+
+      for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(
+          c.currentPatchSetId())) {
+        assertTrue("unexpected reviewer " + psa.getAccountId(),
+            expectedReviewerIds.remove(psa.getAccountId()));
+      }
+      assertTrue("missing reviewers: " + expectedReviewerIds,
+          expectedReviewerIds.isEmpty());
+    }
+
+    public void assertOkStatus() {
+      assertStatus(Status.OK, null);
+    }
+
+    public void assertErrorStatus(String expectedMessage) {
+      assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage);
+    }
+
+    private void assertStatus(Status expectedStatus, String expectedMessage) {
+      RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+      assertEquals(message(refUpdate),
+          expectedStatus, refUpdate.getStatus());
+      assertEquals(expectedMessage, refUpdate.getMessage());
+    }
+
+    public void assertMessage(String expectedMessage) {
+      RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+      assertTrue(message(refUpdate), message(refUpdate).toLowerCase().contains(
+          expectedMessage.toLowerCase()));
+    }
+
+    private String message(RemoteRefUpdate refUpdate) {
+      StringBuilder b = new StringBuilder();
+      if (refUpdate.getMessage() != null) {
+        b.append(refUpdate.getMessage());
+        b.append("\n");
+      }
+      b.append(result.getMessages());
+      return b.toString();
+    }
+  }
+}
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
new file mode 100644
index 0000000..fa85927
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.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.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 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.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.Change;
+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.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.CommitMergeStatus;
+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.ProjectCache;
+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.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+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.eclipse.jgit.transport.RefSpec;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SubmitOnPushIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  private GitRepositoryManager repoManager;
+
+  @Inject
+  private MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
+  private @GerritPersonIdent PersonIdent serverIdent;
+
+  private TestAccount admin;
+  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");
+
+    project = new Project.NameKey("p");
+    initSsh(admin);
+    SshSession sshSession = new SshSession(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 submitOnPush() throws GitAPIException, OrmException,
+      IOException, ConfigInvalidException {
+    grantSubmit(project, "refs/for/refs/heads/master");
+    PushOneCommit.Result r = pushTo("refs/for/master%submit");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/heads/master");
+  }
+
+  @Test
+  public void submitOnPushToRefsMetaConfig() throws GitAPIException,
+      OrmException, IOException, ConfigInvalidException {
+    grantSubmit(project, "refs/for/refs/meta/config");
+
+    git.fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
+    ObjectId objectId = git.getRepository().getRef("refs/meta/config").getObjectId();
+    git.checkout().setName(objectId.getName()).call();
+
+    PushOneCommit.Result r = pushTo("refs/for/refs/meta/config%submit");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/meta/config");
+  }
+
+  @Test
+  public void submitOnPushMergeConflict() throws GitAPIException, OrmException,
+      IOException, ConfigInvalidException {
+    String master = "refs/heads/master";
+    ObjectId objectId = git.getRepository().getRef(master).getObjectId();
+    push(master, "one change", "a.txt", "some content");
+    git.checkout().setName(objectId.getName()).call();
+
+    grantSubmit(project, "refs/for/refs/heads/master");
+    PushOneCommit.Result r =
+        push("refs/for/master%submit", "other change", "a.txt", "other content");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, null, admin);
+    r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage());
+  }
+
+  @Test
+  public void submitOnPushSuccessfulMerge() throws GitAPIException, OrmException,
+      IOException, ConfigInvalidException {
+    String master = "refs/heads/master";
+    ObjectId objectId = git.getRepository().getRef(master).getObjectId();
+    push(master, "one change", "a.txt", "some content");
+    git.checkout().setName(objectId.getName()).call();
+
+    grantSubmit(project, "refs/for/refs/heads/master");
+    PushOneCommit.Result r =
+        push("refs/for/master%submit", "other change", "b.txt", "other content");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertMergeCommit(master, "other change");
+  }
+
+  @Test
+  public void submitOnPushNewPatchSet() throws GitAPIException,
+      OrmException, IOException, ConfigInvalidException {
+    PushOneCommit.Result r =
+        push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
+
+    grantSubmit(project, "refs/for/refs/heads/master");
+    r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt",
+        "other content", r.getChangeId());
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    Change c = Iterables.getOnlyElement(db.changes().byKey(
+        new Change.Key(r.getChangeId())).toList());
+    assertEquals(2, db.patchSets().byChange(c.getId()).toList().size());
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/heads/master");
+  }
+
+  @Test
+  public void submitOnPushNotAllowed_Error() throws GitAPIException,
+      OrmException, IOException {
+    PushOneCommit.Result r = pushTo("refs/for/master%submit");
+    r.assertErrorStatus("submit not allowed");
+  }
+
+  @Test
+  public void submitOnPushNewPatchSetNotAllowed_Error() throws GitAPIException,
+      OrmException, IOException, ConfigInvalidException {
+    PushOneCommit.Result r =
+        push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
+
+    r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt",
+        "other content", r.getChangeId());
+    r.assertErrorStatus("submit not allowed");
+  }
+
+  @Test
+  public void submitOnPushingDraft_Error() throws GitAPIException,
+      OrmException, IOException {
+    PushOneCommit.Result r = pushTo("refs/for/master%draft,submit");
+    r.assertErrorStatus("cannot submit draft");
+  }
+
+  @Test
+  public void submitOnPushToNonExistingBranch_Error() throws GitAPIException,
+      OrmException, IOException {
+    String branchName = "non-existing";
+    PushOneCommit.Result r = pushTo("refs/for/" + branchName + "%submit");
+    r.assertErrorStatus("branch " + branchName + " not found");
+  }
+
+  private void grantSubmit(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.SUBMIT, true);
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    p.add(new PermissionRule(config.resolve(adminGroup)));
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  private void assertSubmitApproval(PatchSet.Id patchSetId) throws OrmException {
+    List<PatchSetApproval> approvals = db.patchSetApprovals().byPatchSet(patchSetId).toList();
+    assertEquals(1, approvals.size());
+    PatchSetApproval a = approvals.get(0);
+    assertEquals(PatchSetApproval.LabelId.SUBMIT.get(), a.getLabel());
+    assertEquals(1, a.getValue());
+    assertEquals(admin.id, a.getAccountId());
+  }
+
+  private void assertCommit(Project.NameKey project, String branch) throws IOException {
+    Repository r = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(r);
+      try {
+        RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+        assertEquals(PushOneCommit.SUBJECT, c.getShortMessage());
+        assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
+        assertEquals(admin.email, c.getCommitterIdent().getEmailAddress());
+      } finally {
+        rw.release();
+      }
+    } finally {
+      r.close();
+    }
+  }
+
+  private void assertMergeCommit(String branch, String subject) throws IOException {
+    Repository r = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(r);
+      try {
+        RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+        assertEquals(2, c.getParentCount());
+        assertEquals("Merge \"" + subject + "\"", c.getShortMessage());
+        assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
+        assertEquals(serverIdent.getEmailAddress(), c.getCommitterIdent().getEmailAddress());
+      } finally {
+        rw.release();
+      }
+    } finally {
+      r.close();
+    }
+  }
+
+  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, ref);
+  }
+
+  private PushOneCommit.Result push(String ref, String subject,
+      String fileName, String content) throws GitAPIException, IOException {
+    PushOneCommit push =
+        new PushOneCommit(db, admin.getIdent(), subject, fileName, content);
+    return push.to(git, ref);
+  }
+
+  private PushOneCommit.Result push(String ref, String subject,
+      String fileName, String content, String changeId) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent(), subject,
+        fileName, content, changeId);
+    return push.to(git, ref);
+  }
+}
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..1677d42
--- /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(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(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(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(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(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/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
new file mode 100644
index 0000000..57e938a
--- /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(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(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(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-antlr/BUCK b/gerrit-antlr/BUCK
new file mode 100644
index 0000000..0e19320
--- /dev/null
+++ b/gerrit-antlr/BUCK
@@ -0,0 +1,43 @@
+ANTLR_OUTS = [
+  'QueryLexer.java',
+  'QueryParser.java',
+]
+PARSER_DEPS = [
+  ':query_antlr',
+  ':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'],
+  outs = ANTLR_OUTS,
+)
+
+# Hack necessary to expose ANTLR generated code as JAR to Eclipse.
+java_library(
+  name = 'lib',
+  srcs = [genfile(f) for f in ANTLR_OUTS],
+  deps = PARSER_DEPS,
+)
+
+genrule(
+  name = 'query_link',
+  cmd = 'ln -s $SRCS $OUT',
+  srcs = [genfile('lib__lib__output/lib.jar')],
+  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
index e1eba40..74696d8 100644
--- a/gerrit-antlr/pom.xml
+++ b/gerrit-antlr/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-antlr</artifactId>
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
index 4abd77c..6f99761 100644
--- a/gerrit-cache-h2/pom.xml
+++ b/gerrit-cache-h2/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-cache-h2</artifactId>
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
new file mode 100644
index 0000000..07bbcae
--- /dev/null
+++ b/gerrit-common/BUCK
@@ -0,0 +1,68 @@
+SRC = 'src/main/java/com/google/gerrit/'
+VER = 'resources/com/google/gerrit/common/Version'
+
+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:jsr305',
+    '//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:jsr305',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'version',
+  resources = [genfile(VER)],
+  deps = [':git_describe'],
+  visibility = ['PUBLIC'],
+)
+
+# TODO(sop): Move git describe into an uncacheable genrule()
+def git_describe():
+  import subprocess
+  cmd = ['git', 'describe', 'HEAD']
+  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
+
+genrule(
+  name = 'git_describe',
+  cmd = 'mkdir -p $(dirname $OUT); echo "%s" >$OUT' % git_describe(),
+  srcs = [],
+  out = VER,
+)
+
+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
index d9173f0..9fee1aa 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
@@ -40,8 +40,9 @@
     </dependency>
 
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
     </dependency>
 
     <dependency>
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 7ac21db..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
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.List;
+import java.util.Set;
 
 /** Detail necessary to display a change. */
 public class ChangeDetail {
@@ -27,6 +28,7 @@
   protected boolean allowsAnonymous;
   protected boolean canAbandon;
   protected boolean canEditCommitMessage;
+  protected boolean canCherryPick;
   protected boolean canPublish;
   protected boolean canRebase;
   protected boolean canRestore;
@@ -37,6 +39,7 @@
   protected List<ChangeInfo> dependsOn;
   protected List<ChangeInfo> neededBy;
   protected List<PatchSet> patchSets;
+  protected Set<PatchSet.Id> patchSetsWithDraftComments;
   protected List<SubmitRecord> submitRecords;
   protected Project.SubmitType submitType;
   protected SubmitTypeRecord submitTypeRecord;
@@ -82,6 +85,14 @@
     canEditCommitMessage = a;
   }
 
+  public boolean canCherryPick() {
+    return canCherryPick;
+  }
+
+  public void setCanCherryPick(final boolean a) {
+    canCherryPick = a;
+  }
+
   public boolean canPublish() {
     return canPublish;
   }
@@ -187,6 +198,14 @@
     patchSets = s;
   }
 
+  public void setPatchSetsWithDraftComments(Set<PatchSet.Id> pwdc) {
+    this.patchSetsWithDraftComments = pwdc;
+  }
+
+  public boolean hasDraftComments(PatchSet.Id id) {
+    return patchSetsWithDraftComments.contains(id);
+  }
+
   public void setSubmitRecords(List<SubmitRecord> all) {
     submitRecords = all;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
index 93f283b..e4d7b80 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.Project;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class GarbageCollectionResult {
   protected List<Error> errors;
 
   public GarbageCollectionResult() {
-    errors = Lists.newArrayList();
+    errors = new ArrayList<Error>();
   }
 
   public void addError(Error e) {
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 16b99dc..7660a80 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
@@ -20,9 +20,7 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 
-import java.util.List;
 import java.util.Set;
 
 public class GerritConfig implements Cloneable {
@@ -44,7 +42,6 @@
   protected String editFullNameUrl;
   protected Project.NameKey wildProject;
   protected Set<Account.FieldName> editableAccountFields;
-  protected List<RegexFindReplace> commentLinks;
   protected boolean documentationAvailable;
   protected boolean testChangeMerge;
   protected String anonymousCowardName;
@@ -188,14 +185,6 @@
     editableAccountFields = af;
   }
 
-  public List<RegexFindReplace> getCommentLinks() {
-    return commentLinks;
-  }
-
-  public void setCommentLinks(final List<RegexFindReplace> cl) {
-    commentLinks = cl;
-  }
-
   public boolean isDocumentationAvailable() {
     return documentationAvailable;
   }
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 3580774..8528c0f 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
@@ -75,6 +75,9 @@
     * project names */
   private char pathSeparator = '/';
 
+  /** Whether to include links to draft patch sets */
+  private boolean linkDrafts;
+
   /** Private default constructor for gson. */
   protected GitWebType() {
   }
@@ -125,6 +128,15 @@
   }
 
   /**
+   * Get whether to link to draft patch sets
+   *
+   * @return True to link
+   */
+  public boolean getLinkDrafts() {
+    return linkDrafts;
+  }
+
+  /**
    * Set the pattern for branch view.
    *
    * @param pattern The pattern for branch view
@@ -201,4 +213,8 @@
   public void setPathSeparator(char separator) {
     this.pathSeparator = separator;
   }
+
+  public void setLinkDrafts(boolean linkDrafts) {
+    this.linkDrafts = linkDrafts;
+  }
 }
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 7db691d..8c08feb 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
@@ -73,6 +73,9 @@
   /** 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";
+
   /** Can view the server's current cache states. */
   public static final String VIEW_CACHES = "viewCaches";
 
@@ -99,6 +102,7 @@
     NAMES_ALL.add(QUERY_LIMIT);
     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);
     NAMES_ALL.add(VIEW_QUEUE);
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/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 2308b77..fecbb76 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
@@ -53,6 +53,7 @@
   protected boolean hugeFile;
   protected boolean intralineDifference;
   protected boolean intralineFailure;
+  protected boolean intralineTimeout;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
@@ -60,7 +61,7 @@
       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 id, final boolean idf, final boolean idt) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -79,6 +80,7 @@
     hugeFile = hf;
     intralineDifference = id;
     intralineFailure = idf;
+    intralineTimeout = idt;
   }
 
   protected PatchScript() {
@@ -152,6 +154,10 @@
     return intralineFailure;
   }
 
+  public boolean hasIntralineTimeout() {
+    return intralineTimeout;
+  }
+
   public boolean isExpandAllComments() {
     return diffPrefs.isExpandAllComments();
   }
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 a2debf2..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
@@ -17,13 +17,17 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 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 {
   protected PatchSet patchSet;
   protected PatchSetInfo info;
   protected List<Patch> patches;
+  protected Project.NameKey project;
+  protected List<UiCommandDetail> commands;
 
   public PatchSetDetail() {
   }
@@ -51,4 +55,23 @@
   public void setPatches(final List<Patch> p) {
     patches = p;
   }
+
+  public Project.NameKey getProject() {
+    return project;
+  }
+
+  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/ProjectAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
index 0638906..f08cf7e 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
@@ -58,11 +58,6 @@
 
   @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/SingleListChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java
deleted file mode 100644
index e55375b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java
+++ /dev/null
@@ -1,52 +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 java.util.List;
-
-/** Summary information needed for screens showing a single list of changes}. */
-public class SingleListChangeInfo {
-  protected AccountInfoCache accounts;
-  protected List<ChangeInfo> changes;
-  protected boolean atEnd;
-
-  public SingleListChangeInfo() {
-  }
-
-  public AccountInfoCache getAccounts() {
-    return accounts;
-  }
-
-  public void setAccounts(final AccountInfoCache ac) {
-    accounts = ac;
-  }
-
-  public List<ChangeInfo> getChanges() {
-    return changes;
-  }
-
-  public boolean isAtEnd() {
-    return atEnd;
-  }
-
-  public void setChanges(List<ChangeInfo> c) {
-    setChanges(c, true);
-  }
-
-  public void setChanges(List<ChangeInfo> c, boolean end) {
-    changes = c;
-    atEnd = end;
-  }
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
similarity index 71%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
rename to gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
index b2fb901..cd011860 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.restapi;
+package com.google.gerrit.common.data;
 
-import java.io.IOException;
-import java.io.OutputStream;
-
-public interface StreamingResponse {
-  public String getContentType();
-  public void stream(OutputStream out) throws IOException;
+/** 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-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
index c47cf07..1829c8b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
@@ -23,4 +23,8 @@
   public NoSuchEntityException() {
     super(MESSAGE);
   }
+
+  public NoSuchEntityException(String message) {
+    super(message);
+  }
 }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
new file mode 100644
index 0000000..75539f6
--- /dev/null
+++ b/gerrit-extension-api/BUCK
@@ -0,0 +1,6 @@
+java_library2(
+  name = 'api',
+  srcs = glob(['src/main/java/com/google/gerrit/extensions/**/*.java']),
+  compile_deps = ['//lib/guice:guice'],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 4f57209..5db8cec 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-extension-api</artifactId>
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..a4c5eb5 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
@@ -22,6 +22,7 @@
 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 +40,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>
@@ -136,6 +137,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/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 188011c..103874f 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,7 @@
   private String characterEncoding;
   private long contentLength = -1;
   private boolean gzip = true;
+  private boolean base64 = false;
 
   /** @return the MIME type of the result, for HTTP clients. */
   public String getContentType() {
@@ -110,6 +111,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/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/webui/UiCommand.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiCommand.java
new file mode 100644
index 0000000..be58c1a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiCommand.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.extensions.webui;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+
+import java.util.Set;
+
+public interface UiCommand<R extends RestResource> extends RestView<R> {
+  public static enum Place {
+    PATCHSET_ACTION_PANEL;
+  };
+
+  Set<Place> getPlaces();
+  String getLabel(R resource);
+  String getTitle(R resource);
+  boolean isVisible(R resource);
+  boolean isEnabled(R resource);
+}
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
index 0622e1b..7b8e595 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
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/.gitignore b/gerrit-gwtexpui/.gitignore
new file mode 100644
index 0000000..406c4d5
--- /dev/null
+++ b/gerrit-gwtexpui/.gitignore
@@ -0,0 +1,6 @@
+/target
+/generated_classes
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..f9fe345
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs b/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/gerrit-gwtexpui/.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/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs b/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..21aa7e7
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,285 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=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_annotation=0
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+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_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+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.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+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_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_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_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+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_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+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_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+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_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+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_on_off_tags=false
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs b/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..7d663fda
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+formatter_profile=_Google Format
+formatter_settings_version=12
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
new file mode 100644
index 0000000..41625ff
--- /dev/null
+++ b/gerrit-gwtexpui/BUCK
@@ -0,0 +1,112 @@
+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'],
+  deps = [
+    ':SafeHtml',
+    ':UserAgent',
+    '//lib/gwt:user',
+    '//lib:LICENSE-clippy',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'clippy_swf',
+  cmd = 'mkdir $TMP/gerrit_ui;' +
+    'cp $SRCS $TMP/gerrit_ui;' +
+    'cd $TMP;' +
+    'zip -qr $OUT gerrit_ui',
+  srcs = [SRC + 'clippy/public/gwtexpui_clippy1.cache.swf'],
+  out = 'clippy_swf.zip',
+  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/COPYING b/gerrit-gwtexpui/COPYING
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/gerrit-gwtexpui/COPYING
@@ -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/gerrit-gwtexpui/pom.xml b/gerrit-gwtexpui/pom.xml
new file mode 100644
index 0000000..2283bcf
--- /dev/null
+++ b/gerrit-gwtexpui/pom.xml
@@ -0,0 +1,75 @@
+<?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.8-SNAPSHOT</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-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
similarity index 69%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
rename to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
index d7e835f..0e9b072 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,8 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name="com.google.gwtexpui.safehtml.SafeHtml"/>
+  <inherits name="com.google.gwtexpui.user.User"/>
 </module>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
similarity index 76%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
index b21d3c0..68495e8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.clippy.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+import com.google.gwt.resources.client.CssResource;
+
+public interface ClippyCss extends CssResource {
+  String label();
+  String control();
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
similarity index 71%
copy from gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
index 42ee5dd..4c2b8981 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common;
+package com.google.gwtexpui.clippy.client;
 
+import com.google.gwt.core.client.GWT;
 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();
+public interface ClippyResources extends ClientBundle {
+  public static final ClippyResources I = GWT.create(ClippyResources.class);
+
+  @Source("clippy.css")
+  ClippyCss css();
 }
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
new file mode 100644
index 0000000..273318b
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -0,0 +1,230 @@
+// 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.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;
+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.dom.client.KeyPressHandler;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasText;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.UserAgent;
+
+/**
+ * Label which permits the user to easily copy the complete content.
+ * <p>
+ * If the Flash plugin is available a "movie" is embedded that provides
+ * one-click copying of the content onto the system clipboard. The label (if
+ * visible) can also be clicked, switching from a label to an input box,
+ * allowing the user to copy the text with a keyboard shortcut.
+ */
+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 {
+    ClippyResources.I.css().ensureInjected();
+  }
+
+  public static boolean isFlashEnabled() {
+    return flashEnabled;
+  }
+
+  public static void setFlashEnabled(final boolean on) {
+    flashEnabled = on;
+  }
+
+  private static String swfUrl() {
+    if (swfUrl == null) {
+      swfUrl = GWT.getModuleBaseURL() + "gwtexpui_clippy1.cache.swf";
+    }
+    return swfUrl;
+  }
+
+  private final FlowPanel content;
+  private String text;
+  private int visibleLen;
+  private Label textLabel;
+  private TextBox textBox;
+  private Element swf;
+
+  /**
+   * Create a new label
+   *
+   * @param str initial content
+   */
+  public CopyableLabel(final String str) {
+    this(str, true);
+  }
+
+  /**
+   * Create a new label
+   *
+   * @param str initial content
+   * @param showLabel if true, the content is shown, if false it is hidden from
+   *        view and only the copy icon is displayed.
+   */
+  public CopyableLabel(final String str, final boolean showLabel) {
+    content = new FlowPanel();
+    initWidget(content);
+
+    text = str;
+    visibleLen = text.length();
+
+    if (showLabel) {
+      textLabel = new InlineLabel(getText());
+      textLabel.setStyleName(ClippyResources.I.css().label());
+      textLabel.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          showTextBox();
+        }
+      });
+      content.add(textLabel);
+    }
+    embedMovie();
+  }
+
+  /**
+   * Change the text which is displayed in the clickable label.
+   *
+   * @param text the new preview text, should be shorter than the original text
+   *        which would be copied to the clipboard.
+   */
+  public void setPreviewText(final String text) {
+    if (textLabel != null) {
+      textLabel.setText(text);
+      visibleLen = text.length();
+    }
+  }
+
+  private void embedMovie() {
+    if (flashEnabled && UserAgent.hasFlash) {
+      final String flashVars = "text=" + URL.encodeQueryString(getText());
+      final SafeHtmlBuilder h = new SafeHtmlBuilder();
+
+      h.openElement("div");
+      h.setStyleName(ClippyResources.I.css().control());
+
+      h.openElement("object");
+      h.setWidth(SWF_WIDTH);
+      h.setHeight(SWF_HEIGHT);
+      h.setAttribute("classid", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000");
+      h.paramElement("movie", swfUrl());
+      h.paramElement("FlashVars", flashVars);
+
+      h.openElement("embed");
+      h.setWidth(SWF_WIDTH);
+      h.setHeight(SWF_HEIGHT);
+      h.setAttribute("wmode", "transparent");
+      h.setAttribute("type", "application/x-shockwave-flash");
+      h.setAttribute("src", swfUrl());
+      h.setAttribute("FlashVars", flashVars);
+      h.closeSelf();
+
+      h.closeElement("object");
+      h.closeElement("div");
+
+      if (swf != null) {
+        DOM.removeChild(getElement(), swf);
+      }
+      DOM.appendChild(getElement(), swf = SafeHtml.parse(h));
+    }
+  }
+
+  public String getText() {
+    return text;
+  }
+
+  public void setText(final String newText) {
+    text = newText;
+    visibleLen = newText.length();
+
+    if (textLabel != null) {
+      textLabel.setText(getText());
+    }
+    if (textBox != null) {
+      textBox.setText(getText());
+      textBox.selectAll();
+    }
+    embedMovie();
+  }
+
+  private void showTextBox() {
+    if (textBox == null) {
+      textBox = new TextBox();
+      textBox.setText(getText());
+      textBox.setVisibleLength(visibleLen);
+      textBox.addKeyPressHandler(new KeyPressHandler() {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
+          if (event.isControlKeyDown() || event.isMetaKeyDown()) {
+            switch (event.getCharCode()) {
+              case 'c':
+              case 'x':
+                Scheduler.get().scheduleDeferred(new Command() {
+                  public void execute() {
+                    hideTextBox();
+                  }
+                });
+                break;
+            }
+          }
+        }
+      });
+      textBox.addBlurHandler(new BlurHandler() {
+        @Override
+        public void onBlur(final BlurEvent event) {
+          hideTextBox();
+        }
+      });
+      content.insert(textBox, 1);
+    }
+
+    textLabel.setVisible(false);
+    textBox.setVisible(true);
+    Scheduler.get().scheduleDeferred(new Command() {
+      @Override
+      public void execute() {
+        textBox.selectAll();
+        textBox.setFocus(true);
+      }
+    });
+  }
+
+  private void hideTextBox() {
+    if (textBox != null) {
+      textBox.removeFromParent();
+      textBox = null;
+    }
+    textLabel.setVisible(true);
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css
new file mode 100644
index 0000000..b962df3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css
@@ -0,0 +1,25 @@
+/* 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.
+ */
+
+.label {
+  vertical-align: top;
+}
+.control {
+  margin-left: 5px;
+  display: inline-block !important;
+  height: 14px;
+  width: 14px;
+  overflow: hidden;
+}
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/public/gwtexpui_clippy1.cache.swf
new file mode 100644
index 0000000..e46886c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
index d7e835f..b385987 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <define-linker name='cachecss' class='com.google.gwtexpui.css.rebind.CssLinker'/>
+  <add-linker name='cachecss'/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
new file mode 100644
index 0000000..0f6992d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
@@ -0,0 +1,130 @@
+// 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.css.rebind;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.Artifact;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.PublicResource;
+import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
+import com.google.gwt.core.ext.linker.impl.StandardStylesheetReference;
+import com.google.gwt.dev.util.Util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+
+@LinkerOrder(LinkerOrder.Order.PRE)
+public class CssLinker extends AbstractLinker {
+  @Override
+  public String getDescription() {
+    return "CssLinker";
+  }
+
+  @Override
+  public ArtifactSet link(final TreeLogger logger, final LinkerContext context,
+      final ArtifactSet artifacts) throws UnableToCompleteException {
+    final ArtifactSet returnTo = new ArtifactSet();
+    int index = 0;
+
+    final HashMap<String, PublicResource> css =
+        new HashMap<String, PublicResource>();
+
+    for (final StandardStylesheetReference ssr : artifacts
+        .<StandardStylesheetReference> find(StandardStylesheetReference.class)) {
+      css.put(ssr.getSrc(), null);
+    }
+    for (final PublicResource pr : artifacts
+        .<PublicResource> find(PublicResource.class)) {
+      if (css.containsKey(pr.getPartialPath())) {
+        css.put(pr.getPartialPath(), new CssPubRsrc(name(logger, pr), pr));
+      }
+    }
+
+    for (Artifact<?> a : artifacts) {
+      if (a instanceof PublicResource) {
+        final PublicResource r = (PublicResource) a;
+        if (css.containsKey(r.getPartialPath())) {
+          a = css.get(r.getPartialPath());
+        }
+      } else if (a instanceof StandardStylesheetReference) {
+        final StandardStylesheetReference r = (StandardStylesheetReference) a;
+        final PublicResource p = css.get(r.getSrc());
+        a = new StandardStylesheetReference(p.getPartialPath(), index);
+      }
+
+      returnTo.add(a);
+      index++;
+    }
+    return returnTo;
+  }
+
+  private String name(final TreeLogger logger, final PublicResource r)
+      throws UnableToCompleteException {
+    final InputStream in = r.getContents(logger);
+    final ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+    try {
+      try {
+        final byte[] buf = new byte[2048];
+        int n;
+        while ((n = in.read(buf)) >= 0) {
+          tmp.write(buf, 0, n);
+        }
+        tmp.close();
+      } finally {
+        in.close();
+      }
+    } catch (IOException e) {
+      final UnableToCompleteException ute = new UnableToCompleteException();
+      ute.initCause(e);
+      throw ute;
+    }
+
+    String base = r.getPartialPath();
+    final int s = base.lastIndexOf('/');
+    if (0 < s) {
+      base = base.substring(0, s + 1);
+    } else {
+      base = "";
+    }
+    return base + Util.computeStrongName(tmp.toByteArray()) + ".cache.css";
+  }
+
+  private static class CssPubRsrc extends PublicResource {
+    private static final long serialVersionUID = 1L;
+    private final PublicResource src;
+
+    CssPubRsrc(final String partialPath, final PublicResource r) {
+      super(StandardLinkerContext.class, partialPath);
+      src = r;
+    }
+
+    @Override
+    public InputStream getContents(final TreeLogger logger)
+        throws UnableToCompleteException {
+      return src.getContents(logger);
+    }
+
+    @Override
+    public long getLastModified() {
+      return src.getLastModified();
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
index d7e835f..771050f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,8 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name='com.google.gwtexpui.user.User'/>
+  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
new file mode 100644
index 0000000..304d56e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
@@ -0,0 +1,40 @@
+// 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.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+
+public final class CompoundKeyCommand extends KeyCommand {
+  final KeyCommandSet set;
+
+  public CompoundKeyCommand(int mask, char key, String help, KeyCommandSet s) {
+    super(mask, key, help);
+    set = s;
+  }
+
+  public CompoundKeyCommand(int mask, int key, String help, KeyCommandSet s) {
+    super(mask, key, help);
+    set = s;
+  }
+
+  public KeyCommandSet getSet() {
+    return set;
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    GlobalKey.temporaryWithTimeout(set);
+  }
+}
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
new file mode 100644
index 0000000..d680a72
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
@@ -0,0 +1,51 @@
+// 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.globalkey.client;
+
+import com.google.gwt.dom.client.Document;
+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.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+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 {
+  private static DocWidget me;
+
+  static DocWidget get() {
+    if (me == null) {
+      me = new DocWidget();
+    }
+    return me;
+  }
+
+  private DocWidget() {
+    setElement((Element) docnode());
+    onAttach();
+    RootPanel.detachOnWindowClose(this);
+  }
+
+  @Override
+  public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+    return addDomHandler(handler, KeyPressEvent.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
new file mode 100644
index 0000000..1eaaa3c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
@@ -0,0 +1,183 @@
+// 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.globalkey.client;
+
+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.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+
+public class GlobalKey {
+  public static final KeyPressHandler STOP_PROPAGATION = new KeyPressHandler() {
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      event.stopPropagation();
+    }
+  };
+
+  private static State global;
+  static State active;
+  private static CloseHandler<PopupPanel> restoreGlobal;
+  private static Timer restoreTimer;
+
+  static {
+    KeyResources.I.css().ensureInjected();
+  }
+
+  private static void initEvents() {
+    if (active == null) {
+      DocWidget.get().addKeyPressHandler(new KeyPressHandler() {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
+          final KeyCommandSet s = active.live;
+          if (s != active.all) {
+            active.live = active.all;
+            restoreTimer.cancel();
+          }
+          s.onKeyPress(event);
+        }
+      });
+
+      restoreTimer = new Timer() {
+        @Override
+        public void run() {
+          active.live = active.all;
+        }
+      };
+
+      global = new State(null);
+      active = global;
+    }
+  }
+
+  private static void initDialog() {
+    if (restoreGlobal == null) {
+      restoreGlobal = new CloseHandler<PopupPanel>() {
+        @Override
+        public void onClose(final CloseEvent<PopupPanel> event) {
+          active = global;
+        }
+      };
+    }
+  }
+
+  static void temporaryWithTimeout(final KeyCommandSet s) {
+    active.live = s;
+    restoreTimer.schedule(250);
+  }
+
+  public static void dialog(final PopupPanel panel) {
+    initEvents();
+    initDialog();
+    assert panel.isShowing();
+    assert active == global;
+    active = new State(panel);
+    active.add(new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, panel));
+    panel.addCloseHandler(restoreGlobal);
+  }
+
+  public static HandlerRegistration addApplication(final Widget widget,
+      final KeyCommand appKey) {
+    initEvents();
+    final State state = stateFor(widget);
+    state.add(appKey);
+    return new HandlerRegistration() {
+      @Override
+      public void removeHandler() {
+        state.remove(appKey);
+      }
+    };
+  }
+
+  public static HandlerRegistration add(final Widget widget,
+      final KeyCommandSet cmdSet) {
+    initEvents();
+    final State state = stateFor(widget);
+    state.add(cmdSet);
+    return new HandlerRegistration() {
+      @Override
+      public void removeHandler() {
+        state.remove(cmdSet);
+      }
+    };
+  }
+
+  private static State stateFor(Widget w) {
+    while (w != null) {
+      if (w == active.root) {
+        return active;
+      }
+      w = w.getParent();
+    }
+    return global;
+  }
+
+  public static void filter(final KeyCommandFilter filter) {
+    active.filter(filter);
+    if (active != global) {
+      global.filter(filter);
+    }
+  }
+
+  private GlobalKey() {
+  }
+
+  static class State {
+    final Widget root;
+    final KeyCommandSet app;
+    final KeyCommandSet all;
+    KeyCommandSet live;
+
+    State(final Widget r) {
+      root = r;
+
+      app = new KeyCommandSet(KeyConstants.I.applicationSection());
+      app.add(ShowHelpCommand.INSTANCE);
+
+      all = new KeyCommandSet();
+      all.add(app);
+
+      live = all;
+    }
+
+    void add(final KeyCommand k) {
+      app.add(k);
+      all.add(k);
+    }
+
+    void remove(final KeyCommand k) {
+      app.remove(k);
+      all.remove(k);
+    }
+
+    void add(final KeyCommandSet s) {
+      all.add(s);
+    }
+
+    void remove(final KeyCommandSet s) {
+      all.remove(s);
+    }
+
+    void filter(final KeyCommandFilter f) {
+      all.filter(f);
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
new file mode 100644
index 0000000..0274b9d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
@@ -0,0 +1,33 @@
+// 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.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/** Hides the given popup panel when invoked. */
+public class HidePopupPanelCommand extends KeyCommand {
+  private final PopupPanel panel;
+
+  public HidePopupPanelCommand(int mask, int key, PopupPanel panel) {
+    super(mask, key, KeyConstants.I.closeCurrentDialog());
+    this.panel = panel;
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    panel.hide();
+  }
+}
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
new file mode 100644
index 0000000..ba4f626
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
@@ -0,0 +1,94 @@
+// 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.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+
+public abstract class KeyCommand implements KeyPressHandler {
+  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 boolean same(final KeyCommand a, final KeyCommand b) {
+    return a.getClass() == b.getClass() && a.helpText.equals(b.helpText);
+  }
+
+  final int keyMask;
+  private final String helpText;
+
+  public KeyCommand(final int mask, final int key, final String help) {
+    this(mask, (char) key, help);
+  }
+
+  public KeyCommand(final int mask, final char key, final String help) {
+    assert help != null;
+    keyMask = mask | key;
+    helpText = help;
+  }
+
+  public String getHelpText() {
+    return helpText;
+  }
+
+  SafeHtml describeKeyStroke() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+
+    if ((keyMask & M_CTRL) == M_CTRL) {
+      modifier(b, KeyConstants.I.keyCtrl());
+    }
+    if ((keyMask & M_ALT) == M_ALT) {
+      modifier(b, KeyConstants.I.keyAlt());
+    }
+    if ((keyMask & M_META) == M_META) {
+      modifier(b, KeyConstants.I.keyMeta());
+    }
+
+    final char c = (char) (keyMask & 0xffff);
+    switch (c) {
+      case KeyCodes.KEY_ENTER:
+        namedKey(b, KeyConstants.I.keyEnter());
+        break;
+      case KeyCodes.KEY_ESCAPE:
+        namedKey(b, KeyConstants.I.keyEsc());
+        break;
+      default:
+        b.openSpan();
+        b.setStyleName(KeyResources.I.css().helpKey());
+        b.append(String.valueOf(c));
+        b.closeSpan();
+        break;
+    }
+
+    return b;
+  }
+
+  private void modifier(final SafeHtmlBuilder b, final String name) {
+    namedKey(b, name);
+    b.append(" + ");
+  }
+
+  private void namedKey(final SafeHtmlBuilder b, final String name) {
+    b.append('<');
+    b.openSpan();
+    b.setStyleName(KeyResources.I.css().helpKey());
+    b.append(name);
+    b.closeSpan();
+    b.append(">");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
similarity index 78%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
index b21d3c0..05f41d4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.globalkey.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+public interface KeyCommandFilter {
+  public boolean include(KeyCommand key);
 }
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
new file mode 100644
index 0000000..4f3205a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -0,0 +1,136 @@
+// 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.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class KeyCommandSet implements KeyPressHandler {
+  private final Map<Integer, KeyCommand> map;
+  private List<KeyCommandSet> sets;
+  private String name;
+
+  public KeyCommandSet() {
+    this("");
+  }
+
+  public KeyCommandSet(final String setName) {
+    map = new HashMap<Integer, KeyCommand>();
+    name = setName;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(final String setName) {
+    assert setName != null;
+    name = setName;
+  }
+
+  public boolean isEmpty() {
+    return map.isEmpty();
+  }
+
+  public void add(final KeyCommand k) {
+    assert !map.containsKey(k.keyMask)
+         : "Key " + k.describeKeyStroke().asString()
+         + " already registered";
+    if (!map.containsKey(k.keyMask)) {
+      map.put(k.keyMask, k);
+    }
+  }
+
+  public void remove(final KeyCommand k) {
+    assert map.get(k.keyMask) == k;
+    map.remove(k.keyMask);
+  }
+
+  public void add(final KeyCommandSet set) {
+    if (sets == null) {
+      sets = new ArrayList<KeyCommandSet>();
+    }
+    assert !sets.contains(set);
+    sets.add(set);
+    for (final KeyCommand k : set.map.values()) {
+      add(k);
+    }
+  }
+
+  public void remove(final KeyCommandSet set) {
+    assert sets != null;
+    assert sets.contains(set);
+    sets.remove(set);
+    for (final KeyCommand k : set.map.values()) {
+      remove(k);
+    }
+  }
+
+  public void filter(final KeyCommandFilter filter) {
+    if (sets != null) {
+      for (final KeyCommandSet s : sets) {
+        s.filter(filter);
+      }
+    }
+    for (final Iterator<KeyCommand> i = map.values().iterator(); i.hasNext();) {
+      final KeyCommand kc = i.next();
+      if (!filter.include(kc)) {
+        i.remove();
+      } else if (kc instanceof CompoundKeyCommand) {
+        ((CompoundKeyCommand) kc).set.filter(filter);
+      }
+    }
+  }
+
+  public Collection<KeyCommand> getKeys() {
+    return map.values();
+  }
+
+  public Collection<KeyCommandSet> getSets() {
+    return sets != null ? sets : Collections.<KeyCommandSet> emptyList();
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    final KeyCommand k = map.get(toMask(event));
+    if (k != null) {
+      event.preventDefault();
+      event.stopPropagation();
+      k.onKeyPress(event);
+    }
+  }
+
+  static int toMask(final KeyPressEvent event) {
+    int mask = event.getCharCode();
+    if (event.isAltKeyDown()) {
+      mask |= KeyCommand.M_ALT;
+    }
+    if (event.isControlKeyDown()) {
+      mask |= KeyCommand.M_CTRL;
+    }
+    if (event.isMetaKeyDown()) {
+      mask |= KeyCommand.M_META;
+    }
+    return mask;
+  }
+}
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
new file mode 100644
index 0000000..56fb85c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
@@ -0,0 +1,37 @@
+// 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.globalkey.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.Constants;
+
+public interface KeyConstants extends Constants {
+  public static final KeyConstants I = GWT.create(KeyConstants.class);
+
+  String applicationSection();
+  String showHelp();
+  String closeCurrentDialog();
+
+  String keyboardShortcuts();
+  String closeButton();
+  String orOtherKey();
+  String thenOtherKey();
+
+  String keyCtrl();
+  String keyAlt();
+  String keyMeta();
+  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
new file mode 100644
index 0000000..e21daf5
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
@@ -0,0 +1,14 @@
+applicationSection = Application
+showHelp = Open shortcut help
+closeCurrentDialog = Close current dialog
+
+keyboardShortcuts = Keyboard Shortcuts
+closeButton = Close
+orOtherKey = or
+thenOtherKey = then
+
+keyCtrl = Ctrl
+keyAlt = Alt
+keyMeta = Meta
+keyEnter = Enter
+keyEsc = Esc
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java
similarity index 62%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java
index b21d3c0..d19018d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java
@@ -12,9 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.globalkey.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+import com.google.gwt.resources.client.CssResource;
+
+public interface KeyCss extends CssResource {
+  String helpPopup();
+  String helpHeader();
+  String helpHeaderGlue();
+  String helpTable();
+  String helpTableGlue();
+  String helpGroup();
+  String helpKeyStroke();
+  String helpSeparator();
+  String helpKey();
 }
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
new file mode 100644
index 0000000..7bd0233
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -0,0 +1,228 @@
+// 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.globalkey.client;
+
+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.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+
+public class KeyHelpPopup extends PluginSafePopupPanel implements
+    KeyPressHandler {
+  private final FocusPanel focus;
+
+  public KeyHelpPopup() {
+    super(true/* autohide */, true/* modal */);
+    setStyleName(KeyResources.I.css().helpPopup());
+
+    final Anchor closer = new Anchor(KeyConstants.I.closeButton());
+    closer.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        hide();
+      }
+    });
+
+    final Grid header = new Grid(1, 3);
+    header.setStyleName(KeyResources.I.css().helpHeader());
+    header.setText(0, 0, KeyConstants.I.keyboardShortcuts());
+    header.setWidget(0, 2, closer);
+
+    final CellFormatter fmt = header.getCellFormatter();
+    fmt.addStyleName(0, 1, KeyResources.I.css().helpHeaderGlue());
+    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+
+    final Grid lists = new Grid(0, 7);
+    lists.setStyleName(KeyResources.I.css().helpTable());
+    populate(lists);
+    lists.getCellFormatter().addStyleName(0, 3,
+        KeyResources.I.css().helpTableGlue());
+
+    final FlowPanel body = new FlowPanel();
+    body.add(header);
+    DOM.appendChild(body.getElement(), DOM.createElement("hr"));
+    body.add(lists);
+
+    focus = new FocusPanel(body);
+    DOM.setStyleAttribute(focus.getElement(), "outline", "0px");
+    DOM.setElementAttribute(focus.getElement(), "hideFocus", "true");
+    focus.addKeyPressHandler(this);
+    add(focus);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    super.setVisible(show);
+    if (show) {
+      focus.setFocus(true);
+    }
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    if (KeyCommandSet.toMask(event) == ShowHelpCommand.INSTANCE.keyMask) {
+      // Block the '?' key from triggering us to show right after
+      // we just hide ourselves.
+      //
+      event.stopPropagation();
+      event.preventDefault();
+    }
+    hide();
+  }
+
+  private void populate(final Grid lists) {
+    int end[] = new int[5];
+    int column = 0;
+    for (final KeyCommandSet set : combinedSetsByName()) {
+      int row = end[column];
+      row = formatGroup(lists, row, column, set);
+      end[column] = row;
+      if (column == 0) {
+        column = 4;
+      } else {
+        column = 0;
+      }
+    }
+  }
+
+  /**
+   * @return an ordered collection of KeyCommandSet, combining sets which share
+   *         the same name, so that each set name appears at most once.
+   */
+  private static Collection<KeyCommandSet> combinedSetsByName() {
+    final LinkedHashMap<String, KeyCommandSet> byName =
+        new LinkedHashMap<String, KeyCommandSet>();
+    for (final KeyCommandSet set : GlobalKey.active.all.getSets()) {
+      KeyCommandSet v = byName.get(set.getName());
+      if (v == null) {
+        v = new KeyCommandSet(set.getName());
+        byName.put(v.getName(), v);
+      }
+      v.add(set);
+    }
+    return byName.values();
+  }
+
+  private int formatGroup(final Grid lists, int row, final int col,
+      final KeyCommandSet set) {
+    if (set.isEmpty()) {
+      return row;
+    }
+
+    if (lists.getRowCount() < row + 1) {
+      lists.resizeRows(row + 1);
+    }
+    lists.setText(row, col + 2, set.getName());
+    lists.getCellFormatter().addStyleName(row, col + 2,
+        KeyResources.I.css().helpGroup());
+    row++;
+
+    return formatKeys(lists, row, col, set, null);
+  }
+
+  private int formatKeys(final Grid lists, int row, final int col,
+      final KeyCommandSet set, final SafeHtml prefix) {
+    final CellFormatter fmt = lists.getCellFormatter();
+    final int initialRow = row;
+    final List<KeyCommand> keys = sort(set);
+    if (lists.getRowCount() < row + keys.size()) {
+      lists.resizeRows(row + keys.size());
+    }
+    FORMAT_KEYS: for (int i = 0; i < keys.size(); i++) {
+      final KeyCommand k = keys.get(i);
+
+      if (k instanceof CompoundKeyCommand) {
+        final SafeHtmlBuilder b = new SafeHtmlBuilder();
+        b.append(k.describeKeyStroke());
+        row = formatKeys(lists, row, col, ((CompoundKeyCommand) k).getSet(), b);
+        continue;
+      }
+
+      for (int prior = 0; prior < i; prior++) {
+        if (KeyCommand.same(keys.get(prior), k)) {
+          final int r = initialRow + prior;
+          final SafeHtmlBuilder b = new SafeHtmlBuilder();
+          b.append(SafeHtml.get(lists, r, col + 0));
+          b.append(" ");
+          b.append(KeyConstants.I.orOtherKey());
+          b.append(" ");
+          if (prefix != null) {
+            b.append(prefix);
+            b.append(" ");
+            b.append(KeyConstants.I.thenOtherKey());
+            b.append(" ");
+          }
+          b.append(k.describeKeyStroke());
+          SafeHtml.set(lists, r, col + 0, b);
+          continue FORMAT_KEYS;
+        }
+      }
+
+      if (prefix != null) {
+        final SafeHtmlBuilder b = new SafeHtmlBuilder();
+        b.append(prefix);
+        b.append(" ");
+        b.append(KeyConstants.I.thenOtherKey());
+        b.append(" ");
+        b.append(k.describeKeyStroke());
+        SafeHtml.set(lists, row, col + 0, b);
+      } else {
+        SafeHtml.set(lists, row, col + 0, k.describeKeyStroke());
+      }
+      lists.setText(row, col + 1, ":");
+      lists.setText(row, col + 2, k.getHelpText());
+
+      fmt.addStyleName(row, col + 0, KeyResources.I.css().helpKeyStroke());
+      fmt.addStyleName(row, col + 1, KeyResources.I.css().helpSeparator());
+      row++;
+    }
+
+    return row;
+  }
+
+  private List<KeyCommand> sort(final KeyCommandSet set) {
+    final List<KeyCommand> keys = new ArrayList<KeyCommand>(set.getKeys());
+    Collections.sort(keys, new Comparator<KeyCommand>() {
+      @Override
+      public int compare(KeyCommand arg0, KeyCommand arg1) {
+        if (arg0.keyMask < arg1.keyMask) {
+          return -1;
+        } else if (arg0.keyMask > arg1.keyMask) {
+          return 1;
+        }
+        return 0;
+      }
+    });
+    return keys;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
similarity index 72%
rename from gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
rename to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
index 42ee5dd..a52ca2a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common;
+package com.google.gwtexpui.globalkey.client;
 
+import com.google.gwt.core.client.GWT;
 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();
+public interface KeyResources extends ClientBundle {
+  public static final KeyResources I = GWT.create(KeyResources.class);
+
+  @Source("key.css")
+  KeyCss css();
 }
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
new file mode 100644
index 0000000..c06d2c4
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
@@ -0,0 +1,34 @@
+// 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.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 {
+  public NpTextArea() {
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
+
+  public NpTextArea(final Element element) {
+    super(element);
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
+
+  public void setSpellCheck(boolean spell) {
+    DOM.setElementPropertyBoolean(getElement(), "spellcheck", spell);
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
similarity index 61%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
index b21d3c0..86402e1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
@@ -12,9 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.globalkey.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.TextBox;
+
+public class NpTextBox extends TextBox {
+  public NpTextBox() {
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
+
+  public NpTextBox(final Element element) {
+    super(element);
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+  }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
new file mode 100644
index 0000000..50a4a86
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
@@ -0,0 +1,61 @@
+// 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.globalkey.client;
+
+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.user.client.Window;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+
+
+public class ShowHelpCommand extends KeyCommand {
+  public static final ShowHelpCommand INSTANCE = new ShowHelpCommand();
+  private static KeyHelpPopup current;
+
+  public ShowHelpCommand() {
+    super(0, '?', KeyConstants.I.showHelp());
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    if (current != null) {
+      // Already open? Close the dialog.
+      //
+      current.hide();
+      current = null;
+      return;
+    }
+
+    final KeyHelpPopup help = new KeyHelpPopup();
+    help.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(final CloseEvent<PopupPanel> event) {
+        current = null;
+      }
+    });
+    current = help;
+    help.setPopupPositionAndShow(new PositionCallback() {
+      @Override
+      public void setPosition(final int pWidth, final int pHeight) {
+        final int left = (Window.getClientWidth() - pWidth) >> 1;
+        final int wLeft = Window.getScrollLeft();
+        final int wTop = Window.getScrollTop();
+        help.setPopupPosition(wLeft + left, wTop + 50);
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css
new file mode 100644
index 0000000..9372e45
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css
@@ -0,0 +1,99 @@
+/* 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.
+ */
+
+@external .popupContent;
+
+.helpPopup {
+  background: #000000 none repeat scroll 0 50%;
+  color: #ffffff;
+  font-family: arial,sans-serif;
+  font-weight: bold;
+  overflow: hidden;
+  text-align: left;
+  text-shadow: 1px 1px 7px #000000;
+  width: 92%;
+  z-index: 1002;
+  opacity: 0.85;
+ }
+
+@if user.agent safari {
+  .helpPopup {
+    \-webkit-border-radius: 10px;
+  }
+}
+@if user.agent gecko1_8 {
+  .helpPopup {
+    \-moz-border-radius: 10px;
+  }
+}
+
+.helpPopup .popupContent {
+  margin: 10px;
+}
+
+.helpPopup hr {
+  width: 100%;
+}
+
+.helpHeader {
+  width: 100%;
+}
+
+.helpHeader td {
+  white-space: nowrap;
+  color: #ffffff;
+}
+
+.helpHeader a,
+.helpHeader a:visited,
+.helpHeader a:hover {
+  color: #dddd00;
+}
+
+.helpHeaderGlue {
+  width: 100%;
+}
+
+.helpTable {
+  width: 90%;
+}
+.helpTable td {
+  vertical-align: top;
+  white-space: nowrap;
+}
+
+.helpTableGlue {
+  width: 25px;
+}
+
+.helpGroup {
+  color: #dddd00;
+  padding-top: 0.8em;
+  text-align: left;
+}
+
+.helpKeyStroke {
+  text-align: right;
+}
+
+.helpSeparator {
+  width: 0.5em;
+  text-align: center;
+  font-weight: bold;
+}
+
+.helpKey {
+  color: #dddd00;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
similarity index 63%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
index d7e835f..a6978ab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
@@ -1,11 +1,11 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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
+ http:..www.apache.org.licenses.LICENSE-2.0
 
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <define-linker name='serverplanned' class='com.google.gwtexpui.linker.rebind.ServerPlannedIFrameLinker'/>
+  <add-linker name='serverplanned'/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java
new file mode 100644
index 0000000..3e2361c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java
@@ -0,0 +1,67 @@
+// 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.linker.rebind;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.CompilationResult;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.StylesheetReference;
+
+import java.util.Map;
+import java.util.SortedMap;
+
+/** Saves data normally used by the {@code nocache.js} file. */
+@LinkerOrder(LinkerOrder.Order.POST)
+public class ServerPlannedIFrameLinker extends AbstractLinker {
+  @Override
+  public String getDescription() {
+    return "ServerPlannedIFrameLinker";
+  }
+
+  @Override
+  public ArtifactSet link(final TreeLogger logger, final LinkerContext context,
+      final ArtifactSet artifacts) throws UnableToCompleteException {
+    ArtifactSet toReturn = new ArtifactSet(artifacts);
+
+    StringBuilder table = new StringBuilder();
+    for (StylesheetReference r : artifacts.find(StylesheetReference.class)) {
+      table.append("css ");
+      table.append(r.getSrc());
+      table.append("\n");
+    }
+
+    for (CompilationResult r : artifacts.find(CompilationResult.class)) {
+      table.append(r.getStrongName() + "\n");
+      for (SortedMap<SelectionProperty, String> p : r.getPropertyMap()) {
+        for (Map.Entry<SelectionProperty, String> e : p.entrySet()) {
+          table.append("  ");
+          table.append(e.getKey().getName());
+          table.append("=");
+          table.append(e.getValue());
+          table.append('\n');
+        }
+      }
+      table.append("\n");
+    }
+
+    toReturn.add(emitString(logger, table.toString(), "permutations"));
+    return toReturn;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java
new file mode 100644
index 0000000..89da529
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java
@@ -0,0 +1,36 @@
+// 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.linker.server;
+
+import javax.servlet.http.HttpServletRequest;
+
+/** A rule that must execute on the client, as we don't know how to compute it. */
+final class ClientSideRule implements Rule {
+  private final String name;
+
+  ClientSideRule(String name) {
+    this.name = name;
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public String select(HttpServletRequest req) {
+    return null;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java
new file mode 100644
index 0000000..b319db1
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java
@@ -0,0 +1,160 @@
+// 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.linker.server;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+/** A single permutation of the compiled GWT application. */
+public class Permutation {
+  private final PermutationSelector selector;
+  private final String cacheHTML;
+  private final String[] values;
+
+  Permutation(PermutationSelector sel, String cacheHTML, String[] values) {
+    this.selector = sel;
+    this.cacheHTML = cacheHTML;
+    this.values = values;
+  }
+
+  boolean matches(String[] r) {
+    return Arrays.equals(values, r);
+  }
+
+  /**
+   * Append GWT bootstrap for this permutation onto the end of the body.
+   * <p>
+   * The GWT bootstrap for this particular permutation is appended onto the end
+   * of the {@code body} element of the passed host page.
+   * <p>
+   * To keep the bootstrap code small and simple, not all GWT features are
+   * actually supported. The {@code gwt:property}, {@code gwt:onPropertyErrorFn}
+   * and {@code gwt:onLoadErrorFn} meta tags are ignored and not handled.
+   * <p>
+   * Load order may differ from the standard GWT {@code nocache.js}. The browser
+   * is asked to load the iframe immediately, rather than after the body has
+   * finished loading.
+   *
+   * @param dom host page HTML document.
+   */
+  public void inject(Document dom) {
+    String moduleName = selector.getModuleName();
+    String moduleFunc = moduleName;
+
+    StringBuilder s = new StringBuilder();
+    s.append("\n");
+    s.append("function " + moduleFunc + "(){");
+    s.append("var s,l,t");
+    s.append(",w=window");
+    s.append(",d=document");
+    s.append(",n='" + moduleName + "'");
+    s.append(",f=d.createElement('iframe')");
+    s.append(";");
+
+    // Callback to execute the module once both s and l are true.
+    //
+    s.append("function m(){");
+    s.append("if(s&&l){");
+    // Base path needs to be absolute. There isn't an easy way to do this
+    // other than forcing an image to load and then pulling the URL back.
+    //
+    s.append("var b,i=d.createElement('img');");
+    s.append("i.src=n+'/clear.cache.gif';");
+    s.append("b=i.src;");
+    s.append("b=b.substring(0,b.lastIndexOf('/')+1);");
+    s.append(moduleFunc + "=null;"); // allow us to GC
+    s.append("f.contentWindow.gwtOnLoad(undefined,n,b);");
+    s.append("}");
+    s.append("}");
+
+    // Set s true when the module script has finished loading. The
+    // exact name here is known to the IFrameLinker and is called by
+    // the code in the iframe.
+    //
+    s.append(moduleFunc + ".onScriptLoad=function(){");
+    s.append("s=1;m();");
+    s.append("};");
+
+    // Set l true when the browser has finished processing the iframe
+    // tag, and everything else on the page.
+    //
+    s.append(moduleFunc + ".r=function(){");
+    s.append("l=1;m();");
+    s.append("};");
+
+    // Prevents mixed mode security in IE6/7.
+    s.append("f.src=\"javascript:''\";");
+    s.append("f.id=n;");
+    s.append("f.style.cssText"
+        + "='position:absolute;width:0;height:0;border:none';");
+    s.append("f.tabIndex=-1;");
+    s.append("d.body.appendChild(f);");
+
+    // The src has to be set after the iframe is attached to the DOM to avoid
+    // refresh quirks in Safari. We have to use the location.replace trick to
+    // avoid FF2 refresh quirks.
+    //
+    s.append("f.contentWindow.location.replace(n+'/" + cacheHTML + "');");
+
+    // defer attribute here is to workaround IE running immediately.
+    //
+    s.append("d.write('<script defer=\"defer\">" //
+        + moduleFunc + ".r()</'+'script>');");
+    s.append("}");
+    s.append(moduleFunc + "();");
+    s.append("\n//");
+
+    final Element html = dom.getDocumentElement();
+    final Element head = (Element) html.getElementsByTagName("head").item(0);
+    final Element body = (Element) html.getElementsByTagName("body").item(0);
+
+    for (String css : selector.getCSS()) {
+      if (isRelativeURL(css)) {
+        css = moduleName + '/' + css;
+      }
+
+      final Element link = dom.createElement("link");
+      link.setAttribute("rel", "stylesheet");
+      link.setAttribute("href", css);
+      head.appendChild(link);
+    }
+
+    final Element script = dom.createElement("script");
+    script.setAttribute("type", "text/javascript");
+    script.setAttribute("language", "javascript");
+    script.appendChild(dom.createComment(s.toString()));
+    body.appendChild(script);
+  }
+
+  private static boolean isRelativeURL(String src) {
+    if (src.startsWith("/")) {
+      return false;
+    }
+
+    try {
+      // If it parses as a URL, assume it is not relative.
+      //
+      new URL(src);
+      return false;
+    } catch (MalformedURLException e) {
+    }
+
+    return true;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java
new file mode 100644
index 0000000..d3e5ae3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java
@@ -0,0 +1,205 @@
+// 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.linker.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Selects a permutation based on the HTTP request.
+ * <p>
+ * To use this class the application's GWT module must include our linker by
+ * inheriting our module:
+ *
+ * <pre>
+ *   &lt;inherits name='com.google.gwtexpui.linker.ServerPlannedIFrameLinker'/&gt;
+ * </pre>
+ */
+public class PermutationSelector {
+  private final String moduleName;
+  private final Map<String, Rule> rulesByName;
+  private final List<Rule> ruleOrder;
+  private final List<Permutation> permutations;
+  private final List<String> css;
+
+  /**
+   * Create an empty selector for a module.
+   * <p>
+   * {@link UserAgentRule} rule is automatically registered. Additional custom
+   * selector rules may be registered before {@link #init(ServletContext)} is
+   * called to finish the selector setup.
+   *
+   * @param moduleName the name of the module within the context.
+   */
+  public PermutationSelector(final String moduleName) {
+    this.moduleName = moduleName;
+
+    this.rulesByName = new HashMap<String, Rule>();
+    this.ruleOrder = new ArrayList<Rule>();
+    this.permutations = new ArrayList<Permutation>();
+    this.css = new ArrayList<String>();
+
+    register(new UserAgentRule());
+  }
+
+  private void notInitialized() {
+    if (!ruleOrder.isEmpty()) {
+      throw new IllegalStateException("Already initialized");
+    }
+  }
+
+  /**
+   * Register a property selection rule.
+   *
+   * @param r the rule implementation.
+   */
+  public void register(Rule r) {
+    notInitialized();
+    rulesByName.put(r.getName(), r);
+  }
+
+  /**
+   * Initialize the selector by reading the module's {@code permutations} file.
+   *
+   * @param ctx context to load the module data from.
+   * @throws ServletException
+   * @throws IOException
+   */
+  public void init(ServletContext ctx) throws ServletException, IOException {
+    notInitialized();
+
+    final String tableName = "/" + moduleName + "/permutations";
+    final InputStream in = ctx.getResourceAsStream(tableName);
+    if (in == null) {
+      throw new ServletException("No " + tableName + " in context");
+    }
+    try {
+      BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+      for (;;) {
+        final String strongName = r.readLine();
+        if (strongName == null) {
+          break;
+        }
+
+        if (strongName.startsWith("css ")) {
+          css.add(strongName.substring("css ".length()));
+          continue;
+        }
+
+        Map<String, String> selections = new LinkedHashMap<String, String>();
+        for (;;) {
+          String permutation = r.readLine();
+          if (permutation == null || permutation.isEmpty()) {
+            break;
+          }
+
+          int eq = permutation.indexOf('=');
+          if (eq < 0) {
+            throw new ServletException(tableName + " has malformed content");
+          }
+
+          String k = permutation.substring(0, eq).trim();
+          String v = permutation.substring(eq + 1);
+
+          Rule rule = get(k);
+          if (!ruleOrder.contains(rule)) {
+            ruleOrder.add(rule);
+          }
+
+          if (selections.put(k, v) != null) {
+            throw new ServletException("Table " + tableName
+                + " has multiple values for " + k + " within permutation "
+                + strongName);
+          }
+        }
+
+        String cacheHtml = strongName + ".cache.html";
+        String[] values = new String[ruleOrder.size()];
+        for (int i = 0; i < values.length; i++) {
+          values[i] = selections.get(ruleOrder.get(i).getName());
+        }
+        permutations.add(new Permutation(this, cacheHtml, values));
+      }
+    } finally {
+      in.close();
+    }
+  }
+
+  private Rule get(final String name) {
+    Rule r = rulesByName.get(name);
+    if (r == null) {
+      r = new ClientSideRule(name);
+      register(r);
+    }
+    return r;
+  }
+
+  /** @return name of the module (within the application context). */
+  public String getModuleName() {
+    return moduleName;
+  }
+
+  /** @return all possible permutations */
+  public List<Permutation> getPermutations() {
+    return Collections.unmodifiableList(permutations);
+  }
+
+  /**
+   * Select the permutation that best matches the browser request.
+   *
+   * @param req current request.
+   * @return the selected permutation; null if no permutation can be fit to the
+   *         request and the standard {@code nocache.js} loader must be used.
+   */
+  public Permutation select(HttpServletRequest req) {
+    final String[] values = new String[ruleOrder.size()];
+    for (int i = 0; i < values.length; i++) {
+      final String value = ruleOrder.get(i).select(req);
+      if (value == null) {
+        // If the rule returned null it doesn't know how to compute
+        // the value for this HTTP request. Since we can't do that
+        // defer to JavaScript by not picking a permutation.
+        //
+        return null;
+      }
+      values[i] = value;
+    }
+
+    for (Permutation p : permutations) {
+      if (p.matches(values)) {
+        return p;
+      }
+    }
+
+    return null;
+  }
+
+  Collection<String> getCSS() {
+    return css;
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java
new file mode 100644
index 0000000..76b9b51
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java
@@ -0,0 +1,40 @@
+// 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.linker.server;
+
+import javax.servlet.http.HttpServletRequest;
+
+/** A selection rule for a permutation property. */
+public interface Rule {
+  /** @return the property name, for example {@code "user.agent"}. */
+  public String getName();
+
+  /**
+   * Compute the value for this property, given the current request.
+   * <p>
+   * This rule method must compute the proper permutation value, matching what
+   * the GWT module XML files use for this property. The rule may use any state
+   * available in the current servlet request.
+   * <p>
+   * If this method returns {@code null} server side selection will be aborted
+   * and selection for all properties will be handled on the client side by the
+   * {@code nocache.js} file.
+   *
+   * @param req the request
+   * @return the value for the property; null if the value cannot be determined
+   *         on the server side.
+   */
+  public String select(HttpServletRequest req);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
new file mode 100644
index 0000000..366b6c5
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
@@ -0,0 +1,93 @@
+// 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.linker.server;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import static java.util.regex.Pattern.compile;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Selects the value for the {@code user.agent} property.
+ * <p>
+ * Examines the {@code User-Agent} HTTP request header, and tries to match it to
+ * known {@code user.agent} values.
+ * <p>
+ * Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}.
+ */
+public class UserAgentRule implements Rule {
+  private static final Pattern msie = compile(".*msie ([0-9]+)\\.([0-9]+).*");
+  private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*");
+
+  public String getName() {
+    return "user.agent";
+  }
+
+  @Override
+  public String select(HttpServletRequest req) {
+    String ua = req.getHeader("User-Agent");
+    if (ua == null) {
+      return null;
+    }
+
+    ua = ua.toLowerCase();
+
+    if (ua.indexOf("opera") != -1) {
+      return "opera";
+
+    } else if (ua.indexOf("webkit") != -1) {
+      return "safari";
+
+    } else if (ua.indexOf("msie") != -1) {
+      // GWT 2.0 uses document.documentMode here, which we can't do
+      // on the server side.
+
+      Matcher m = msie.matcher(ua);
+      if (m.matches() && m.groupCount() == 2) {
+        int v = makeVersion(m);
+        if (v >= 10000) {
+          return "ie10";
+        }
+        if (v >= 9000) {
+          return "ie9";
+        }
+        if (v >= 8000) {
+          return "ie8";
+        }
+        if (v >= 6000) {
+          return "ie6";
+        }
+      }
+      return null;
+
+    } else if (ua.indexOf("gecko") != -1) {
+      Matcher m = gecko.matcher(ua);
+      if (m.matches() && m.groupCount() == 2) {
+        if (makeVersion(m) >= 1008) {
+          return "gecko1_8";
+        }
+      }
+      return "gecko";
+    }
+
+    return null;
+  }
+
+  private int makeVersion(Matcher result) {
+    return (Integer.parseInt(result.group(1)) * 1000)
+        + Integer.parseInt(result.group(2));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
index d7e835f..0df8928 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name="com.google.gwt.user.User"/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java
new file mode 100644
index 0000000..5e13f55
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java
@@ -0,0 +1,77 @@
+// 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.progress.client;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Label;
+
+/**
+ * A simple progress bar with a text label.
+ * <p>
+ * The bar is 200 pixels wide and 20 pixels high. To keep the implementation
+ * simple and lightweight this dimensions are fixed and shouldn't be modified by
+ * style overrides in client code or CSS.
+ */
+public class ProgressBar extends Composite {
+  static {
+    ProgressResources.I.css().ensureInjected();
+  }
+
+  private final String callerText;
+  private final Label bar;
+  private final Label msg;
+  private int value;
+
+  /** Create a bar with no message text. */
+  public ProgressBar() {
+    this("");
+  }
+
+  /** Create a bar displaying the specified message. */
+  public ProgressBar(final String text) {
+    if (text == null || text.length() == 0) {
+      callerText = "";
+    } else {
+      callerText = text + " ";
+    }
+
+    final FlowPanel body = new FlowPanel();
+    body.setStyleName(ProgressResources.I.css().container());
+
+    msg = new Label(callerText);
+    msg.setStyleName(ProgressResources.I.css().text());
+    body.add(msg);
+
+    bar = new Label("");
+    bar.setStyleName(ProgressResources.I.css().bar());
+    body.add(bar);
+
+    initWidget(body);
+  }
+
+  /** @return the current value of the progress meter. */
+  public int getValue() {
+    return value;
+  }
+
+  /** Update the bar's percent completion. */
+  public void setValue(final int pComplete) {
+    assert 0 <= pComplete && pComplete <= 100;
+    value = pComplete;
+    bar.setWidth("" + (2 * pComplete) + "px");
+    msg.setText(callerText + pComplete + "%");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
similarity index 74%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
index b21d3c0..9de2748 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.progress.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+import com.google.gwt.resources.client.CssResource;
+
+public interface ProgressCss extends CssResource {
+  String container();
+  String text();
+  String bar();
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
similarity index 70%
copy from gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
index 42ee5dd..0276e9a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common;
+package com.google.gwtexpui.progress.client;
 
+import com.google.gwt.core.client.GWT;
 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();
+public interface ProgressResources extends ClientBundle {
+  public static final ProgressResources I = GWT.create(ProgressResources.class);
+
+  @Source("progress.css")
+  ProgressCss css();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css
new file mode 100644
index 0000000..683396e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css
@@ -0,0 +1,43 @@
+/* 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.
+ */
+
+.container {
+  position: relative;
+  border: 1px solid #6B90DA;
+  height: 20px;
+  width: 200px;
+}
+
+.text {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+  width: 200px;
+  padding-bottom: 3px;
+  text-align: center;
+  font-weight: bold;
+  font-style: italic;
+  font-size: smaller;
+}
+
+.bar {
+  background: #F0F7F9;
+  border-right: 1px solid #D0D7D9;
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 20px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
index d7e835f..0df8928 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
@@ -1,5 +1,5 @@
 <!--
- Copyright (C) 2011 The Android Open Source Project
+ 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.
@@ -13,8 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="gecko1_8" />
-  <set-property name="locale" value="default" />
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <inherits name="com.google.gwt.user.User"/>
 </module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
new file mode 100644
index 0000000..46d7f51
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
@@ -0,0 +1,137 @@
+// 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.safehtml.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/** Lightweight map of names/values for element attribute construction. */
+class AttMap {
+  private static final Tag ANY = new AnyTag();
+  private static final HashMap<String, Tag> TAGS;
+  static {
+    final Tag src = new SrcTag();
+    TAGS = new HashMap<String, Tag>();
+    TAGS.put("a", new AnchorTag());
+    TAGS.put("form", new FormTag());
+    TAGS.put("img", src);
+    TAGS.put("script", src);
+    TAGS.put("frame", src);
+  }
+
+  private final ArrayList<String> names = new ArrayList<String>();
+  private final ArrayList<String> values = new ArrayList<String>();
+
+  private Tag tag = ANY;
+  private int live;
+
+  void reset(final String tagName) {
+    tag = TAGS.get(tagName.toLowerCase());
+    if (tag == null) {
+      tag = ANY;
+    }
+    live = 0;
+  }
+
+  void onto(final Buffer raw, final SafeHtmlBuilder esc) {
+    for (int i = 0; i < live; i++) {
+      final String v = values.get(i);
+      if (v.length() > 0) {
+        raw.append(" ");
+        raw.append(names.get(i));
+        raw.append("=\"");
+        esc.append(v);
+        raw.append("\"");
+      }
+    }
+  }
+
+  String get(String name) {
+    name = name.toLowerCase();
+
+    for (int i = 0; i < live; i++) {
+      if (name.equals(names.get(i))) {
+        return values.get(i);
+      }
+    }
+    return "";
+  }
+
+  void set(String name, final String value) {
+    name = name.toLowerCase();
+    tag.assertSafe(name, value);
+
+    for (int i = 0; i < live; i++) {
+      if (name.equals(names.get(i))) {
+        values.set(i, value);
+        return;
+      }
+    }
+
+    final int i = live++;
+    if (names.size() < live) {
+      names.add(name);
+      values.add(value);
+    } else {
+      names.set(i, name);
+      values.set(i, value);
+    }
+  }
+
+  private static void assertNotJavascriptUrl(final String value) {
+    if (value.startsWith("#")) {
+      // common in GWT, and safe, so bypass further checks
+
+    } else if (value.trim().toLowerCase().startsWith("javascript:")) {
+      // possibly unsafe, we could have random user code here
+      // we can't tell if its safe or not so we refuse to accept
+      //
+      throw new RuntimeException("javascript unsafe in href: " + value);
+    }
+  }
+
+  private static interface Tag {
+    void assertSafe(String name, String value);
+  }
+
+  private static class AnyTag implements Tag {
+    public void assertSafe(String name, String value) {
+    }
+  }
+
+  private static class AnchorTag implements Tag {
+    public void assertSafe(String name, String value) {
+      if ("href".equals(name)) {
+        assertNotJavascriptUrl(value);
+      }
+    }
+  }
+
+  private static class FormTag implements Tag {
+    public void assertSafe(String name, String value) {
+      if ("action".equals(name)) {
+        assertNotJavascriptUrl(value);
+      }
+    }
+  }
+
+  private static class SrcTag implements Tag {
+    public void assertSafe(String name, String value) {
+      if ("src".equals(name)) {
+        assertNotJavascriptUrl(value);
+      }
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
similarity index 69%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
index b21d3c0..d79c580 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
@@ -12,9 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.safehtml.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+interface Buffer {
+  void append(boolean v);
+
+  void append(char v);
+
+  void append(int v);
+
+  void append(long v);
+
+  void append(float v);
+
+  void append(double v);
+
+  void append(String v);
+
+  String toString();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
new file mode 100644
index 0000000..a1801ad
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
@@ -0,0 +1,56 @@
+// 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.safehtml.client;
+
+final class BufferDirect implements Buffer {
+  private final StringBuilder strbuf = new StringBuilder();
+
+  boolean isEmpty() {
+    return strbuf.length() == 0;
+  }
+
+  public void append(final boolean v) {
+    strbuf.append(v);
+  }
+
+  public void append(final char v) {
+    strbuf.append(v);
+  }
+
+  public void append(final int v) {
+    strbuf.append(v);
+  }
+
+  public void append(final long v) {
+    strbuf.append(v);
+  }
+
+  public void append(final float v) {
+    strbuf.append(v);
+  }
+
+  public void append(final double v) {
+    strbuf.append(v);
+  }
+
+  public void append(final String v) {
+    strbuf.append(v);
+  }
+
+  @Override
+  public String toString() {
+    return strbuf.toString();
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
new file mode 100644
index 0000000..6b5346d8
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
@@ -0,0 +1,56 @@
+// 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.safehtml.client;
+
+final class BufferSealElement implements Buffer {
+  private final SafeHtmlBuilder shb;
+
+  BufferSealElement(final SafeHtmlBuilder safeHtmlBuilder) {
+    shb = safeHtmlBuilder;
+  }
+
+  public void append(final boolean v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final char v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final double v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final float v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final int v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final long v) {
+    shb.sealElement().append(v);
+  }
+
+  public void append(final String v) {
+    shb.sealElement().append(v);
+  }
+
+  @Override
+  public String toString() {
+    return shb.sealElement().toString();
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
new file mode 100644
index 0000000..f7bc907
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.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.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/** A Find/Replace pair used against the {@link SafeHtml} block of text. */
+public interface FindReplace {
+  /**
+   * @return regular expression to match substrings with; should be treated as
+   *     immutable.
+   */
+  public RegExp pattern();
+
+  /**
+   * Find and replace a single instance of this pattern in an input.
+   * <p>
+   * <b>WARNING:</b> No XSS sanitization is done on the return value of this
+   * method, e.g. this value may be passed directly to
+   * {@link SafeHtml#replaceAll(String, String)}. Implementations must sanitize output
+   * appropriately.
+   *
+   * @param input input string.
+   * @return result of regular expression replacement.
+   * @throws IllegalArgumentException if the input could not be safely sanitized.
+   */
+  public String replace(String input);
+}
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
new file mode 100644
index 0000000..e2c576b
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -0,0 +1,96 @@
+// 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.safehtml.client;
+
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+import java.util.ArrayList;
+
+/**
+ * A suggestion oracle that tries to highlight the matched text.
+ * <p>
+ * Suggestions supplied by the implementation of
+ * {@link #onRequestSuggestions(Request, Callback)} are modified to wrap all
+ * occurrences of the {@link SuggestOracle.Request#getQuery()} substring in HTML
+ * <code>&lt;strong&gt;</code> tags, so they can be emphasized to the user.
+ */
+public abstract class HighlightSuggestOracle extends SuggestOracle {
+  private static String escape(String ds) {
+    return new SafeHtmlBuilder().append(ds).asString();
+  }
+
+  @Override
+  public final boolean isDisplayStringHTML() {
+    return true;
+  }
+
+  @Override
+  public final void requestSuggestions(final Request request, final Callback cb) {
+    onRequestSuggestions(request, new Callback() {
+      public void onSuggestionsReady(final Request request,
+          final Response response) {
+        final String qpat = getQueryPattern(request.getQuery());
+        final boolean html = isHTML();
+        final ArrayList<Suggestion> r = new ArrayList<Suggestion>();
+        for (final Suggestion s : response.getSuggestions()) {
+          r.add(new BoldSuggestion(qpat, s, html));
+        }
+        cb.onSuggestionsReady(request, new Response(r));
+      }
+    });
+  }
+
+  protected String getQueryPattern(final String query) {
+    return "(" + escape(query) + ")";
+  }
+
+  /**
+   * @return true if {@link SuggestOracle.Suggestion#getDisplayString()} returns
+   *         HTML; false if the text must be escaped before evaluating in an
+   *         HTML like context.
+   */
+  protected boolean isHTML() {
+    return false;
+  }
+
+  /** Compute the suggestions and return them for display. */
+  protected abstract void onRequestSuggestions(Request request, Callback done);
+
+  private static class BoldSuggestion implements Suggestion {
+    private final Suggestion suggestion;
+    private final String displayString;
+
+    BoldSuggestion(final String qstr, final Suggestion s, final boolean html) {
+      suggestion = s;
+
+      String ds = s.getDisplayString();
+      if (!html) {
+        ds = escape(ds);
+      }
+      displayString = sgi(ds, qstr, "<strong>$1</strong>");
+    }
+
+    private static native String sgi(String inString, String pat, String newHtml)
+    /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/;
+
+    public String getDisplayString() {
+      return displayString;
+    }
+
+    public String getReplacementString() {
+      return suggestion.getReplacementString();
+    }
+  }
+}
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
new file mode 100644
index 0000000..eaa4f23
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.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.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/**
+ * A Find/Replace pair whose replacement string is a link.
+ * <p>
+ * It is safe to pass arbitrary user-provided links to this class. Links are
+ * sanitized as follows:
+ * <ul>
+ * <li>Only http(s) and mailto links are supported; any other scheme results in
+ * an {@link IllegalArgumentException} from {@link #replace(String)}.
+ * <li>Special characters in the link after regex replacement are escaped with
+ * {@link SafeHtmlBuilder}.</li>
+ * </ul>
+ */
+public class LinkFindReplace implements FindReplace {
+  public static boolean hasValidScheme(String link) {
+    int colon = link.indexOf(':');
+    if (colon < 0) {
+      return true;
+    }
+    String scheme = link.substring(0, colon);
+    return "http".equalsIgnoreCase(scheme)
+        || "https".equalsIgnoreCase(scheme)
+        || "mailto".equalsIgnoreCase(scheme);
+  }
+
+  private RegExp pat;
+  private String link;
+
+  protected LinkFindReplace() {
+  }
+
+  /**
+   * @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>.
+   */
+  public LinkFindReplace(String find, String link) {
+    this.pat = RegExp.compile(find);
+    this.link = link;
+  }
+
+  @Override
+  public RegExp pattern() {
+    return pat;
+  }
+
+  @Override
+  public String replace(String input) {
+    String href = pat.replace(input, link);
+    if (!hasValidScheme(href)) {
+      throw new IllegalArgumentException(
+          "Invalid scheme (" + toString() + "): " + href);
+    }
+    String result = new SafeHtmlBuilder()
+        .openAnchor()
+        .setAttribute("href", href)
+        .append(SafeHtml.asis(input))
+        .closeAnchor()
+        .asString();
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return "find = " + pat.getSource() + ", link = " + link;
+  }
+}
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
new file mode 100644
index 0000000..d22fef6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
@@ -0,0 +1,55 @@
+// 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.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/**
+ * A Find/Replace pair whose replacement string is arbitrary HTML.
+ * <p>
+ * <b>WARNING:</b> This class is not safe used with user-provided patterns.
+ */
+public class RawFindReplace implements FindReplace {
+  private RegExp pat;
+  private String replace;
+
+  protected RawFindReplace() {
+  }
+
+  /**
+   * @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>.
+   */
+  public RawFindReplace(String find, String replace) {
+    this.pat = RegExp.compile(find);
+    this.replace = replace;
+  }
+
+  @Override
+  public RegExp pattern() {
+    return pat;
+  }
+
+  @Override
+  public String replace(String input) {
+    return pat.replace(input, replace);
+  }
+
+  @Override
+  public String toString() {
+    return "find = " + pat.getSource() + ", replace = " + replace;
+  }
+}
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
new file mode 100644
index 0000000..0a9f7a2
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -0,0 +1,302 @@
+// 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.safehtml.client;
+
+import com.google.gwt.core.client.GWT;
+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;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.Iterator;
+import java.util.List;
+
+/** Immutable string safely placed as HTML without further escaping. */
+public abstract class SafeHtml {
+  public static final SafeHtmlResources RESOURCES;
+
+  static {
+    if (GWT.isClient()) {
+      RESOURCES = GWT.create(SafeHtmlResources.class);
+      RESOURCES.css().ensureInjected();
+
+    } else {
+      RESOURCES = new SafeHtmlResources() {
+        @Override
+        public SafeHtmlCss css() {
+          return new SafeHtmlCss() {
+            public String wikiList() {
+              return "wikiList";
+            }
+
+            public String wikiPreFormat() {
+              return "wikiPreFormat";
+            }
+
+            public boolean ensureInjected() {
+              return false;
+            }
+
+            public String getName() {
+              return null;
+            }
+
+            public String getText() {
+              return null;
+            }
+          };
+        }
+      };
+    }
+  }
+
+  /** @return the existing HTML property of a widget. */
+  public static SafeHtml get(final HasHTML t) {
+    return new SafeHtmlString(t.getHTML());
+  }
+
+  /** @return the existing HTML text, wrapped in a safe buffer. */
+  public static SafeHtml asis(final String htmlText) {
+    return new SafeHtmlString(htmlText);
+  }
+
+  /** Set the HTML property of a widget. */
+  public static <T extends HasHTML> T set(final T e, final SafeHtml str) {
+    e.setHTML(str.asString());
+    return e;
+  }
+
+  /** @return the existing inner HTML of any element. */
+  public static SafeHtml get(final Element e) {
+    return new SafeHtmlString(DOM.getInnerHTML(e));
+  }
+
+  /** Set the inner HTML of any element. */
+  public static Element set(final Element e, final SafeHtml str) {
+    DOM.setInnerHTML(e, str.asString());
+    return e;
+  }
+
+  /** @return the existing inner HTML of a table cell. */
+  public static SafeHtml get(final HTMLTable t, final int row, final int col) {
+    return new SafeHtmlString(t.getHTML(row, col));
+  }
+
+  /** Set the inner HTML of a table cell. */
+  public static <T extends HTMLTable> T set(final T t, final int row,
+      final int col, final SafeHtml str) {
+    t.setHTML(row, col, str.asString());
+    return t;
+  }
+
+  /** 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));
+  }
+
+  /** Convert bare http:// and https:// URLs into &lt;a href&gt; tags. */
+  public SafeHtml linkify() {
+    final String part = "(?:" +
+    "[a-zA-Z0-9$_.+!*',%;:@=?#/~-]" +
+    "|&(?!lt;|gt;)" +
+    ")";
+    return replaceAll(
+        "(https?://" +
+          part + "{2,}" +
+          "(?:[(]" + part + "*" + "[)])*" +
+          part + "*" +
+        ")",
+        "<a href=\"$1\" target=\"_blank\">$1</a>");
+  }
+
+  /**
+   * Apply {@link #linkify()}, and "\n\n" to &lt;p&gt;.
+   * <p>
+   * Lines that start with whitespace are assumed to be preformatted, and are
+   * formatted by the {@link SafeHtmlCss#wikiPreFormat()} CSS class.
+   */
+  public SafeHtml wikify() {
+    final SafeHtmlBuilder r = new SafeHtmlBuilder();
+    for (final String p : linkify().asString().split("\n\n")) {
+      if (isPreFormat(p)) {
+        r.openElement("p");
+        for (final String line : p.split("\n")) {
+          r.openSpan();
+          r.setStyleName(RESOURCES.css().wikiPreFormat());
+          r.append(asis(line));
+          r.closeSpan();
+          r.br();
+        }
+        r.closeElement("p");
+
+      } else if (isList(p)) {
+        wikifyList(r, p);
+
+      } else {
+        r.openElement("p");
+        r.append(asis(p));
+        r.closeElement("p");
+      }
+    }
+    return r.toSafeHtml();
+  }
+
+  private void wikifyList(final SafeHtmlBuilder r, final String p) {
+    boolean in_ul = false;
+    boolean in_p = false;
+    for (String line : p.split("\n")) {
+      if (line.startsWith("-") || line.startsWith("*")) {
+        if (!in_ul) {
+          if (in_p) {
+            in_p = false;
+            r.closeElement("p");
+          }
+
+          in_ul = true;
+          r.openElement("ul");
+          r.setStyleName(RESOURCES.css().wikiList());
+        }
+        line = line.substring(1).trim();
+
+      } else if (!in_ul) {
+        if (!in_p) {
+          in_p = true;
+          r.openElement("p");
+        } else {
+          r.append(' ');
+        }
+        r.append(asis(line));
+        continue;
+      }
+
+      r.openElement("li");
+      r.append(asis(line));
+      r.closeElement("li");
+    }
+
+    if (in_ul) {
+      r.closeElement("ul");
+    } else if (in_p) {
+      r.closeElement("p");
+    }
+  }
+
+  private static boolean isPreFormat(final String p) {
+    return p.contains("\n ") || p.contains("\n\t") || p.startsWith(" ")
+        || p.startsWith("\t");
+  }
+
+  private static boolean isList(final String p) {
+    return p.contains("\n- ") || p.contains("\n* ") || p.startsWith("- ")
+        || p.startsWith("* ");
+  }
+
+  /**
+   * Replace first occurrence of <code>regex</code> with <code>repl</code> .
+   * <p>
+   * <b>WARNING:</b> This replacement is being performed against an otherwise
+   * safe HTML string. The caller must ensure that the replacement does not
+   * introduce cross-site scripting attack entry points.
+   *
+   * @param regex regular expression pattern to match the substring with.
+   * @param repl replacement expression. Capture groups within
+   *        <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+   * @return a new string, after the replacement has been made.
+   */
+  public SafeHtml replaceFirst(final String regex, final String repl) {
+    return new SafeHtmlString(asString().replaceFirst(regex, repl));
+  }
+
+  /**
+   * Replace each occurrence of <code>regex</code> with <code>repl</code> .
+   * <p>
+   * <b>WARNING:</b> This replacement is being performed against an otherwise
+   * safe HTML string. The caller must ensure that the replacement does not
+   * introduce cross-site scripting attack entry points.
+   *
+   * @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>.
+   * @return a new string, after the replacements have been made.
+   */
+  public SafeHtml replaceAll(final String regex, final String repl) {
+    return new SafeHtmlString(asString().replaceAll(regex, repl));
+  }
+
+  /**
+   * Replace all find/replace pairs in the list in a single pass.
+   *
+   * @param findReplaceList find/replace pairs to use.
+   * @return a new string, after the replacements have been made.
+   */
+  public <T> SafeHtml replaceAll(List<? extends FindReplace> findReplaceList) {
+    if (findReplaceList == null || findReplaceList.isEmpty()) {
+      return this;
+    }
+
+    StringBuilder pat = new StringBuilder();
+    Iterator<? extends FindReplace> it = findReplaceList.iterator();
+    while (it.hasNext()) {
+      FindReplace fr = it.next();
+      pat.append(fr.pattern().getSource());
+      if (it.hasNext()) {
+        pat.append('|');
+      }
+    }
+
+    StringBuilder result = new StringBuilder();
+    RegExp re = RegExp.compile(pat.toString(), "g");
+    String orig = asString();
+    int index = 0;
+    MatchResult mat;
+    while ((mat = re.exec(orig)) != null) {
+      String g = mat.getGroup(0);
+      // Re-run each candidate to find which one matched.
+      for (FindReplace fr : findReplaceList) {
+        if (fr.pattern().test(g)) {
+          try {
+            String repl = fr.replace(g);
+            result.append(orig.substring(index, mat.getIndex()));
+            result.append(repl);
+          } catch (IllegalArgumentException e) {
+            continue;
+          }
+          index = mat.getIndex() + g.length();
+          break;
+        }
+      }
+    }
+    result.append(orig.substring(index, orig.length()));
+    return asis(result.toString());
+  }
+
+  /** @return a GWT block display widget displaying this HTML. */
+  public Widget toBlockWidget() {
+    return new HTML(asString());
+  }
+
+  /** @return a GWT inline display widget displaying this HTML. */
+  public Widget toInlineWidget() {
+    return new InlineHTML(asString());
+  }
+
+  /** @return a clean HTML string safe for inclusion in any context. */
+  public abstract String asString();
+}
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
new file mode 100644
index 0000000..9fe3267
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -0,0 +1,411 @@
+// 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.safehtml.client;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * Safely constructs a {@link SafeHtml}, escaping user provided content.
+ */
+public class SafeHtmlBuilder extends SafeHtml {
+  private static final Impl impl;
+
+  static {
+    if (GWT.isClient()) {
+      impl = new ClientImpl();
+    } else {
+      impl = new ServerImpl();
+    }
+  }
+
+  private final BufferDirect dBuf;
+  private Buffer cb;
+
+  private BufferSealElement sBuf;
+  private AttMap att;
+
+  public SafeHtmlBuilder() {
+    cb = dBuf = new BufferDirect();
+  }
+
+  /** @return true if this builder has not had an append occur yet. */
+  public boolean isEmpty() {
+    return dBuf.isEmpty();
+  }
+
+  /** @return true if this builder has content appended into it. */
+  public boolean hasContent() {
+    return !isEmpty();
+  }
+
+  public SafeHtmlBuilder append(final boolean in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final char in) {
+    switch (in) {
+      case '&':
+        cb.append("&amp;");
+        break;
+
+      case '>':
+        cb.append("&gt;");
+        break;
+
+      case '<':
+        cb.append("&lt;");
+        break;
+
+      case '"':
+        cb.append("&quot;");
+        break;
+
+      case '\'':
+        cb.append("&#39;");
+        break;
+
+      default:
+        cb.append(in);
+        break;
+    }
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final int in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final long in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final float in) {
+    cb.append(in);
+    return this;
+  }
+
+  public SafeHtmlBuilder append(final double in) {
+    cb.append(in);
+    return this;
+  }
+
+  /** Append already safe HTML as-is, avoiding double escaping. */
+  public SafeHtmlBuilder append(final SafeHtml in) {
+    if (in != null) {
+      cb.append(in.asString());
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final String in) {
+    if (in != null) {
+      impl.escapeStr(this, in);
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final StringBuilder in) {
+    if (in != null) {
+      append(in.toString());
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final StringBuffer in) {
+    if (in != null) {
+      append(in.toString());
+    }
+    return this;
+  }
+
+  /** Append the result of toString(), escaping unsafe characters. */
+  public SafeHtmlBuilder append(final Object in) {
+    if (in != null) {
+      append(in.toString());
+    }
+    return this;
+  }
+
+  /** Append the string, escaping unsafe characters. */
+  public SafeHtmlBuilder append(final CharSequence in) {
+    if (in != null) {
+      escapeCS(this, in);
+    }
+    return this;
+  }
+
+  /**
+   * Open an element, appending "<tagName>" to the buffer.
+   * <p>
+   * After the element is open the attributes may be manipulated until the next
+   * <code>append</code>, <code>openElement</code>, <code>closeSelf</code> or
+   * <code>closeElement</code> call.
+   *
+   * @param tagName name of the HTML element to open.
+   */
+  public SafeHtmlBuilder openElement(final String tagName) {
+    assert isElementName(tagName);
+    cb.append("<");
+    cb.append(tagName);
+    if (sBuf == null) {
+      att = new AttMap();
+      sBuf = new BufferSealElement(this);
+    }
+    att.reset(tagName);
+    cb = sBuf;
+    return this;
+  }
+
+  /**
+   * Get an attribute of the last opened element.
+   *
+   * @param name name of the attribute to read.
+   * @return the attribute value, as a string. The empty string if the attribute
+   *         has not been assigned a value. The returned string is the raw
+   *         (unescaped) value.
+   */
+  public String getAttribute(final String name) {
+    assert isAttributeName(name);
+    assert cb == sBuf;
+    return att.get(name);
+  }
+
+  /**
+   * Set an attribute of the last opened element.
+   *
+   * @param name name of the attribute to set.
+   * @param value value to assign; any existing value is replaced. The value is
+   *        escaped (if necessary) during the assignment.
+   */
+  public SafeHtmlBuilder setAttribute(final String name, final String value) {
+    assert isAttributeName(name);
+    assert cb == sBuf;
+    att.set(name, value != null ? value : "");
+    return this;
+  }
+
+  /**
+   * Set an attribute of the last opened element.
+   *
+   * @param name name of the attribute to set.
+   * @param value value to assign, any existing value is replaced.
+   */
+  public SafeHtmlBuilder setAttribute(final String name, final int value) {
+    return setAttribute(name, String.valueOf(value));
+  }
+
+  /**
+   * Append a new value into a whitespace delimited attribute.
+   * <p>
+   * If the attribute is not yet assigned, this method sets the attribute. If
+   * the attribute is already assigned, the new value is appended onto the end,
+   * after appending a single space to delimit the values.
+   *
+   * @param name name of the attribute to append onto.
+   * @param value additional value to append.
+   */
+  public SafeHtmlBuilder appendAttribute(final String name, String value) {
+    if (value != null && value.length() > 0) {
+      final String e = getAttribute(name);
+      return setAttribute(name, e.length() > 0 ? e + " " + value : value);
+    }
+    return this;
+  }
+
+  /** Set the height attribute of the current element. */
+  public SafeHtmlBuilder setHeight(final int height) {
+    return setAttribute("height", height);
+  }
+
+  /** Set the width attribute of the current element. */
+  public SafeHtmlBuilder setWidth(final int width) {
+    return setAttribute("width", width);
+  }
+
+  /** Set the CSS class name for this element. */
+  public SafeHtmlBuilder setStyleName(final String style) {
+    assert isCssName(style);
+    return setAttribute("class", style);
+  }
+
+  /**
+   * Add an additional CSS class name to this element.
+   *<p>
+   * If no CSS class name has been specified yet, this method initializes it to
+   * the single name.
+   */
+  public SafeHtmlBuilder addStyleName(final String style) {
+    assert isCssName(style);
+    return appendAttribute("class", style);
+  }
+
+  private void sealElement0() {
+    assert cb == sBuf;
+    cb = dBuf;
+    att.onto(cb, this);
+  }
+
+  Buffer sealElement() {
+    sealElement0();
+    cb.append(">");
+    return cb;
+  }
+
+  /** Close the current element with a self closing suffix ("/ &gt;"). */
+  public SafeHtmlBuilder closeSelf() {
+    sealElement0();
+    cb.append(" />");
+    return this;
+  }
+
+  /** Append a closing tag for the named element. */
+  public SafeHtmlBuilder closeElement(final String name) {
+    assert isElementName(name);
+    cb.append("</");
+    cb.append(name);
+    cb.append(">");
+    return this;
+  }
+
+  /** Append "&amp;nbsp;" - a non-breaking space, useful in empty table cells. */
+  public SafeHtmlBuilder nbsp() {
+    cb.append("&nbsp;");
+    return this;
+  }
+
+  /** Append "&lt;br /&gt;" - a line break with no attributes */
+  public SafeHtmlBuilder br() {
+    cb.append("<br />");
+    return this;
+  }
+
+  /** Append "&lt;tr&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openTr() {
+    return openElement("tr");
+  }
+
+  /** Append "&lt;/tr&gt;" */
+  public SafeHtmlBuilder closeTr() {
+    return closeElement("tr");
+  }
+
+  /** Append "&lt;td&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openTd() {
+    return openElement("td");
+  }
+
+  /** Append "&lt;/td&gt;" */
+  public SafeHtmlBuilder closeTd() {
+    return closeElement("td");
+  }
+
+  /** Append "&lt;div&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openDiv() {
+    return openElement("div");
+  }
+
+  /** Append "&lt;/div&gt;" */
+  public SafeHtmlBuilder closeDiv() {
+    return closeElement("div");
+  }
+
+  /** Append "&lt;span&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openSpan() {
+    return openElement("span");
+  }
+
+  /** Append "&lt;/span&gt;" */
+  public SafeHtmlBuilder closeSpan() {
+    return closeElement("span");
+  }
+
+  /** Append "&lt;a&gt;"; attributes may be set if needed */
+  public SafeHtmlBuilder openAnchor() {
+    return openElement("a");
+  }
+
+  /** Append "&lt;/a&gt;" */
+  public SafeHtmlBuilder closeAnchor() {
+    return closeElement("a");
+  }
+
+  /** Append "&lt;param name=... value=... /&gt;". */
+  public SafeHtmlBuilder paramElement(final String name, final String value) {
+    openElement("param");
+    setAttribute("name", name);
+    setAttribute("value", value);
+    return closeSelf();
+  }
+
+  /** @return an immutable {@link SafeHtml} representation of the buffer. */
+  public SafeHtml toSafeHtml() {
+    return new SafeHtmlString(asString());
+  }
+
+  @Override
+  public String asString() {
+    return cb.toString();
+  }
+
+  private static void escapeCS(final SafeHtmlBuilder b, final CharSequence in) {
+    for (int i = 0; i < in.length(); i++) {
+      b.append(in.charAt(i));
+    }
+  }
+
+  private static boolean isElementName(final String name) {
+    return name.matches("^[a-zA-Z][a-zA-Z0-9_-]*$");
+  }
+
+  private static boolean isAttributeName(final String name) {
+    return isElementName(name);
+  }
+
+  private static boolean isCssName(final String name) {
+    return isElementName(name);
+  }
+
+  private static abstract class Impl {
+    abstract void escapeStr(SafeHtmlBuilder b, String in);
+  }
+
+  private static class ServerImpl extends Impl {
+    @Override
+    void escapeStr(final SafeHtmlBuilder b, final String in) {
+      SafeHtmlBuilder.escapeCS(b, in);
+    }
+  }
+
+  private static class ClientImpl extends Impl {
+    @Override
+    void escapeStr(final SafeHtmlBuilder b, final String in) {
+      b.cb.append(escape(in));
+    }
+
+    private static native String escape(String src)
+    /*-{ return src.replace(/&/g,'&amp;')
+                   .replace(/>/g,'&gt;')
+                   .replace(/</g,'&lt;')
+                   .replace(/"/g,'&quot;')
+                   .replace(/'/g,'&#39;');
+     }-*/;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
similarity index 75%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
index b21d3c0..f6836a0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.safehtml.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+import com.google.gwt.resources.client.CssResource;
+
+public interface SafeHtmlCss extends CssResource {
+  String wikiPreFormat();
+  String wikiList();
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
similarity index 74%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
index b21d3c0..e3f5724 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.safehtml.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface SafeHtmlResources extends ClientBundle {
+  @Source("safehtml.css")
+  SafeHtmlCss css();
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
similarity index 72%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
copy to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
index b21d3c0..a229421 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
@@ -12,9 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.safehtml.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+class SafeHtmlString extends SafeHtml {
+  private final String html;
+
+  SafeHtmlString(final String h) {
+    html = h;
+  }
+
+  @Override
+  public String asString() {
+    return html;
+  }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css
new file mode 100644
index 0000000..fcad92c
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css
@@ -0,0 +1,23 @@
+/* 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.
+ */
+
+.wikiPreFormat {
+  white-space: pre;
+  font-family: 'Lucida Console', 'Lucida Sans Typewriter', Monaco, monospace;
+  font-size: small;
+}
+
+.wikiList {
+}
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
new file mode 100644
index 0000000..c4d681f
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
@@ -0,0 +1,106 @@
+// 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.gwtexpui.server;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+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;
+
+/**
+ * Forces GWT resources to cache for a very long time.
+ * <p>
+ * GWT compiled JavaScript and ImageBundles can be cached indefinitely by a
+ * browser and/or an edge proxy, as they never contain user-specific data and
+ * are named by a unique checksum. If their content is ever modified then the
+ * URL changes, so user agents would request a different resource. We force
+ * these resources to have very long expiration times.
+ * <p>
+ * To use, add the following block to your <code>web.xml</code>:
+ *
+ * <pre>
+ * &lt;filter&gt;
+ *     &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
+ *     &lt;filter-class&gt;com.google.gwtexpui.server.CacheControlFilter&lt;/filter-class&gt;
+ *   &lt;/filter&gt;
+ *   &lt;filter-mapping&gt;
+ *     &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
+ *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ *   &lt;/filter-mapping&gt;
+ * </pre>
+ */
+public class CacheControlFilter implements Filter {
+  public void init(final FilterConfig config) {
+  }
+
+  public void destroy() {
+  }
+
+  public void doFilter(final ServletRequest sreq, final ServletResponse srsp,
+      final FilterChain chain) throws IOException, ServletException {
+    final HttpServletRequest req = (HttpServletRequest) sreq;
+    final HttpServletResponse rsp = (HttpServletResponse) srsp;
+    final String pathInfo = pathInfo(req);
+
+    if (cacheForever(pathInfo, req)) {
+      CacheHeaders.setCacheable(req, rsp, 365, TimeUnit.DAYS);
+    } else if (nocache(pathInfo)) {
+      CacheHeaders.setNotCacheable(rsp);
+    }
+
+    chain.doFilter(req, rsp);
+  }
+
+  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")) {
+      return true;
+    } else if (pathInfo.endsWith(".nocache.js")) {
+      final String v = req.getParameter("content");
+      return v != null && v.length() > 20;
+    }
+    return false;
+  }
+
+  private static boolean nocache(final String pathInfo) {
+    if (pathInfo.endsWith(".nocache.js")) {
+      return true;
+    }
+    return false;
+  }
+
+  private static String pathInfo(final HttpServletRequest req) {
+    final String uri = req.getRequestURI();
+    final String ctx = req.getContextPath();
+    return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
+  }
+}
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
new file mode 100644
index 0000000..11409e8
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS 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.server;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Utilities to manage HTTP caching directives in responses. */
+public class CacheHeaders {
+  private static final long MAX_CACHE_DURATION = DAYS.toSeconds(365);
+
+  /**
+   * Do not cache the response, anywhere.
+   *
+   * @param res response being returned.
+   */
+  public static void setNotCacheable(HttpServletResponse res) {
+    String cc = "no-cache, no-store, max-age=0, must-revalidate";
+    res.setHeader("Cache-Control", cc);
+    res.setHeader("Pragma", "no-cache");
+    res.setHeader("Expires", "Fri, 01 Jan 1990 00:00:00 GMT");
+    res.setDateHeader("Date", System.currentTimeMillis());
+  }
+
+  /**
+   * 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}.
+   */
+  public static void setCacheable(
+      HttpServletRequest req, HttpServletResponse res,
+      long age, TimeUnit unit) {
+    if (req.isSecure()) {
+      setCacheablePrivate(res, age, unit);
+    } else {
+      setCacheablePublic(res, age, unit);
+    }
+  }
+
+  /**
+   * Allow the response to be cached by proxies and user-agents.
+   * <p>
+   * If the response includes a Set-Cookie header the cookie may be cached by a
+   * proxy and returned to multiple browsers behind the same proxy. This is
+   * insecure for authenticated connections.
+   *
+   * @param res response being returned.
+   * @param age how long the response can be cached.
+   * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   */
+  public static void setCacheablePublic(HttpServletResponse res,
+      long age, TimeUnit unit) {
+    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);
+  }
+
+  /**
+   * Allow the response to be cached only by the user-agent.
+   *
+   * @param res response being returned.
+   * @param age how long the response can be cached.
+   * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+   */
+  public static void setCacheablePrivate(HttpServletResponse res,
+      long age, TimeUnit unit) {
+    long now = System.currentTimeMillis();
+    res.setDateHeader("Expires", now);
+    res.setDateHeader("Date", now);
+    cache(res, "private", age, unit);
+  }
+
+  private static void cache(HttpServletResponse res,
+      String type, long age, TimeUnit unit) {
+    res.setHeader("Cache-Control", String.format(
+        "%s, max-age=%d",
+        type, maxAgeSeconds(age, unit)));
+  }
+
+  private static long maxAgeSeconds(long age, TimeUnit unit) {
+    return Math.min(unit.toSeconds(age), MAX_CACHE_DURATION);
+  }
+
+  private CacheHeaders() {
+  }
+}
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
new file mode 100644
index 0000000..c681d89
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
@@ -0,0 +1,27 @@
+<!--
+ 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.
+-->
+<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/AutoCenterDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
new file mode 100644
index 0000000..78ea8d6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
@@ -0,0 +1,76 @@
+// 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.gwtexpui.user.client;
+
+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.user.client.Window;
+
+/** A DialogBox that automatically re-centers itself if the window changes */
+public class AutoCenterDialogBox extends PluginSafeDialogBox {
+  private HandlerRegistration recenter;
+
+  public AutoCenterDialogBox() {
+    this(false);
+  }
+
+  public AutoCenterDialogBox(final boolean autoHide) {
+    this(autoHide, true);
+  }
+
+  public AutoCenterDialogBox(final boolean autoHide, final boolean modal) {
+    super(autoHide, modal);
+  }
+
+  @Override
+  public void show() {
+    if (recenter == null) {
+      recenter = Window.addResizeHandler(new ResizeHandler() {
+        @Override
+        public void onResize(final ResizeEvent event) {
+          final int w = event.getWidth();
+          final int h = event.getHeight();
+          AutoCenterDialogBox.this.onResize(w, h);
+        }
+      });
+    }
+    super.show();
+  }
+
+  @Override
+  protected void onUnload() {
+    if (recenter != null) {
+      recenter.removeHandler();
+      recenter = null;
+    }
+    super.onUnload();
+  }
+
+  /**
+   * Invoked when the outer browser window resizes.
+   * <p>
+   * Subclasses may override (but should ensure they still call super.onResize)
+   * to implement custom logic when a window resize occurs.
+   *
+   * @param width new browser window width
+   * @param height new browser window height
+   */
+  protected void onResize(final int width, final int height) {
+    if (isAttached()) {
+      center();
+    }
+  }
+}
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
new file mode 100644
index 0000000..c6ab09a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
@@ -0,0 +1,65 @@
+// 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.core.client.GWT;
+import com.google.gwt.user.client.ui.DialogBox;
+
+/**
+ * A DialogBox that can appear over Flash movies and Java applets.
+ * <p>
+ * Some browsers have issues with placing a &lt;div&gt; (such as that used by
+ * the DialogBox implementation) over top of native UI such as that used by the
+ * Flash plugin. Often the native UI leaks over top of the &lt;div&gt;, which is
+ * not the desired behavior for a dialog box.
+ * <p>
+ * This implementation hides the native resources by setting their display
+ * property to 'none' when the dialog is shown, and restores them back to their
+ * prior setting when the dialog is hidden.
+ * */
+public class PluginSafeDialogBox extends DialogBox {
+  private final PluginSafeDialogBoxImpl impl =
+      GWT.create(PluginSafeDialogBoxImpl.class);
+
+  public PluginSafeDialogBox() {
+    this(false);
+  }
+
+  public PluginSafeDialogBox(final boolean autoHide) {
+    this(autoHide, true);
+  }
+
+  public PluginSafeDialogBox(final boolean autoHide, final boolean modal) {
+    super(autoHide, modal);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    impl.visible(show);
+    super.setVisible(show);
+  }
+
+  @Override
+  public void show() {
+    impl.visible(true);
+    super.show();
+  }
+
+  @Override
+  public void hide(final boolean autoClosed) {
+    impl.visible(false);
+    super.hide(autoClosed);
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
similarity index 78%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
rename to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
index b21d3c0..a32fc99 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gwtexpui.user.client;
 
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
+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
new file mode 100644
index 0000000..e32fe78
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
@@ -0,0 +1,86 @@
+// 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
new file mode 100644
index 0000000..7d9c9fc
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
@@ -0,0 +1,65 @@
+// 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.core.client.GWT;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/**
+ * A PopupPanel that can appear over Flash movies and Java applets.
+ * <p>
+ * Some browsers have issues with placing a &lt;div&gt; (such as that used by
+ * the PopupPanel implementation) over top of native UI such as that used by the
+ * Flash plugin. Often the native UI leaks over top of the &lt;div&gt;, which is
+ * not the desired behavior for a dialog box.
+ * <p>
+ * This implementation hides the native resources by setting their display
+ * property to 'none' when the dialog is shown, and restores them back to their
+ * prior setting when the dialog is hidden.
+ * */
+public class PluginSafePopupPanel extends PopupPanel {
+  private final PluginSafeDialogBoxImpl impl =
+      GWT.create(PluginSafeDialogBoxImpl.class);
+
+  public PluginSafePopupPanel() {
+    this(false);
+  }
+
+  public PluginSafePopupPanel(final boolean autoHide) {
+    this(autoHide, true);
+  }
+
+  public PluginSafePopupPanel(final boolean autoHide, final boolean modal) {
+    super(autoHide, modal);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    impl.visible(show);
+    super.setVisible(show);
+  }
+
+  @Override
+  public void show() {
+    impl.visible(true);
+    super.show();
+  }
+
+  @Override
+  public void hide(final boolean autoClosed) {
+    impl.visible(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
new file mode 100644
index 0000000..02ba9ae
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -0,0 +1,81 @@
+// 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.core.client.GWT;
+import com.google.gwt.user.client.Window;
+
+/**
+ * User agent feature tests we don't create permutations for.
+ * <p>
+ * Some features aren't worth creating full permutations in GWT for, as each new
+ * boolean permutation (only two settings) doubles the compile time required. If
+ * the setting only affects a couple of lines of JavaScript code, the slightly
+ * larger cache files for user agents that lack the functionality requested is
+ * trivial compared to the time developers lose building their application.
+ */
+public class UserAgent {
+  /** Does the browser have ShockwaveFlash plugin enabled? */
+  public static final boolean hasFlash = hasFlash();
+
+  private static native boolean hasFlash()
+  /*-{
+    if (navigator.plugins && navigator.plugins.length) {
+      if (navigator.plugins['Shockwave Flash'])     return true;
+      if (navigator.plugins['Shockwave Flash 2.0']) return true;
+
+    } else if (navigator.mimeTypes && navigator.mimeTypes.length) {
+      var mimeType = navigator.mimeTypes['application/x-shockwave-flash'];
+      if (mimeType && mimeType.enabledPlugin) return true;
+
+    } else {
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.7'); return true; } catch (e) {}
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); return true; } catch (e) {}
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash');   return true; } catch (e) {}
+    }
+    return false;
+  }-*/;
+
+  /**
+   * Test for and disallow running this application in an &lt;iframe&gt;.
+   * <p>
+   * If the application is running within an iframe this method requests a
+   * browser generated redirect to pop the application out of the iframe into
+   * the top level window, and then aborts execution by throwing an exception.
+   * This is call should be placed early within the module's onLoad() method,
+   * before any real UI can be initialized that an attacking site could try to
+   * snip out and present in a confusing context.
+   * <p>
+   * If the break out works, execution will restart automatically in a proper
+   * top level window, where the script has full control over the display. If
+   * the break out fails, execution will abort and stop immediately, preventing
+   * UI widgets from being created, leaving the user with an empty frame.
+   */
+  public static void assertNotInIFrame() {
+    if (GWT.isScript() && amInsideIFrame()) {
+      bustOutOfIFrame(Window.Location.getHref());
+      throw new RuntimeException();
+    }
+  }
+
+  private static native boolean amInsideIFrame()
+  /*-{ return top.location != $wnd.location; }-*/;
+
+  private static native void bustOutOfIFrame(String newloc)
+  /*-{ top.location.href = newloc }-*/;
+
+  private UserAgent() {
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java
new file mode 100644
index 0000000..35ecb12
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java
@@ -0,0 +1,57 @@
+// 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.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * Widget to display within a {@link ViewSite}.
+ *<p>
+ * Implementations must override <code>protected void onLoad()</code> and
+ * arrange for {@link #display()} to be invoked once the DOM within the view is
+ * consistent for presentation to the user. Typically this means that the
+ * subclass can start RPCs within <code>onLoad()</code> and then invoke
+ * <code>display()</code> from within the AsyncCallback's
+ * <code>onSuccess(Object)</code> method.
+ */
+public abstract class View extends Composite {
+  ViewSite<? extends View> site;
+
+  @Override
+  protected void onUnload() {
+    site = null;
+    super.onUnload();
+  }
+
+  /** true if this is the current view of its parent view site */
+  public final boolean isCurrentView() {
+    Widget p = getParent();
+    while (p != null) {
+      if (p instanceof ViewSite<?>) {
+        return ((ViewSite<?>) p).getView() == this;
+      }
+      p = p.getParent();
+    }
+    return false;
+  }
+
+  /** Replace the current view in the parent ViewSite with this view. */
+  public final void display() {
+    if (site != null) {
+      site.swap(this);
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java
new file mode 100644
index 0000000..30b8408f
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java
@@ -0,0 +1,87 @@
+// 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.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+/**
+ * Hosts a single {@link View}.
+ * <p>
+ * View instances are attached inside of an invisible DOM node, permitting their
+ * <code>onLoad()</code> method to be invoked and to update the DOM prior to the
+ * elements being made visible in the UI.
+ * <p>
+ * Complaint View instances must invoke {@link View#display()} once the DOM is
+ * ready for presentation.
+ */
+public class ViewSite<V extends View> extends Composite {
+  private final FlowPanel main;
+  private SimplePanel current;
+  private SimplePanel next;
+
+  public ViewSite() {
+    main = new FlowPanel();
+    initWidget(main);
+  }
+
+  /** Get the current view; null if there is no view being displayed. */
+  @SuppressWarnings("unchecked")
+  public V getView() {
+    return current != null ? (V) current.getWidget() : null;
+  }
+
+  /**
+   * Set the next view to display.
+   * <p>
+   * The view will be attached to the DOM tree within a hidden container,
+   * permitting its <code>onLoad()</code> method to execute and update the DOM
+   * without the user seeing the result.
+   *
+   * @param view the next view to display.
+   */
+  public void setView(final V view) {
+    if (next != null) {
+      main.remove(next);
+    }
+    view.site = this;
+    next = new SimplePanel();
+    next.setVisible(false);
+    main.add(next);
+    next.add(view);
+  }
+
+  /**
+   * Invoked after the view becomes the current view and has been made visible.
+   *
+   * @param view the view being displayed.
+   */
+  protected void onShowView(final V view) {
+  }
+
+  @SuppressWarnings("unchecked")
+  final void swap(final View v) {
+    if (next != null && next.getWidget() == v) {
+      if (current != null) {
+        main.remove(current);
+      }
+      current = next;
+      next = null;
+      current.setVisible(true);
+      onShowView((V) v);
+    }
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
new file mode 100644
index 0000000..97f816f
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.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.gwtexpui.safehtml.client;
+
+import static com.google.gwtexpui.safehtml.client.LinkFindReplace.hasValidScheme;
+
+import junit.framework.TestCase;
+
+public class LinkFindReplaceTest extends TestCase {
+  public void testNoEscaping() {
+    String find = "find";
+    String link = "link";
+    LinkFindReplace a = new LinkFindReplace(find, link);
+    assertEquals(find, a.pattern().getSource());
+    assertEquals("<a href=\"link\">find</a>", a.replace(find));
+    assertEquals("find = " + find + ", link = " + link, a.toString());
+  }
+
+  public void testBackreference() {
+    assertEquals("<a href=\"/bug?id=123\">issue 123</a>",
+        new LinkFindReplace("(bug|issue)\\s*([0-9]+)", "/bug?id=$2")
+            .replace("issue 123"));
+  }
+
+  public void testHasValidScheme() {
+    assertTrue(hasValidScheme("/absolute/path"));
+    assertTrue(hasValidScheme("relative/path"));
+    assertTrue(hasValidScheme("http://url/"));
+    assertTrue(hasValidScheme("HTTP://url/"));
+    assertTrue(hasValidScheme("https://url/"));
+    assertTrue(hasValidScheme("mailto://url/"));
+    assertFalse(hasValidScheme("ftp://url/"));
+    assertFalse(hasValidScheme("data:evil"));
+    assertFalse(hasValidScheme("javascript:alert(1)"));
+  }
+
+  public void testInvalidSchemeInReplace() {
+    try {
+      new LinkFindReplace("find", "javascript:alert(1)").replace("find");
+      fail("Expected IllegalStateException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testInvalidSchemeWithBackreference() {
+    try {
+      new LinkFindReplace(".*(script:[^;]*)", "java$1")
+          .replace("Look at this script: alert(1);");
+      fail("Expected IllegalStateException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testReplaceEscaping() {
+    assertEquals("<a href=\"a&quot;&amp;&#39;&lt;&gt;b\">find</a>",
+        new LinkFindReplace("find", "a\"&'<>b").replace("find"));
+  }
+
+  public void testHtmlInFind() {
+    String rawFind = "<b>&quot;bold&quot;</b>";
+    LinkFindReplace a = new LinkFindReplace(rawFind, "/bold");
+    assertEquals(rawFind, a.pattern().getSource());
+    assertEquals("<a href=\"/bold\">" + rawFind + "</a>", a.replace(rawFind));
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
new file mode 100644
index 0000000..9c450bd
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
@@ -0,0 +1,28 @@
+// 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.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class RawFindReplaceTest extends TestCase {
+  public void testFindReplace() {
+    final String find = "find";
+    final String replace = "replace";
+    final RawFindReplace a = new RawFindReplace(find, replace);
+    assertEquals(find, a.pattern().getSource());
+    assertEquals(replace, a.replace(find));
+    assertEquals("find = " + find + ", replace = " + replace, a.toString());
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
new file mode 100644
index 0000000..a6b0012
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
@@ -0,0 +1,265 @@
+// 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.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtmlBuilderTest extends TestCase {
+  public void testEmpty() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertTrue(b.isEmpty());
+    assertFalse(b.hasContent());
+    assertEquals("", b.asString());
+
+    b.append("a");
+    assertTrue(b.hasContent());
+    assertEquals("a", b.asString());
+  }
+
+  public void testToSafeHtml() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    b.append(1);
+
+    final SafeHtml h = b.toSafeHtml();
+    assertNotNull(h);
+    assertNotSame(h, b);
+    assertFalse(h instanceof SafeHtmlBuilder);
+    assertEquals("1", h.asString());
+  }
+
+  public void testAppend_boolean() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(true));
+    assertSame(b, b.append(false));
+    assertEquals("truefalse", b.asString());
+  }
+
+  public void testAppend_char() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append('a'));
+    assertSame(b, b.append('b'));
+    assertEquals("ab", b.asString());
+  }
+
+  public void testAppend_int() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(4));
+    assertSame(b, b.append(2));
+    assertSame(b, b.append(-100));
+    assertEquals("42-100", b.asString());
+  }
+
+  public void testAppend_long() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(4L));
+    assertSame(b, b.append(2L));
+    assertEquals("42", b.asString());
+  }
+
+  public void testAppend_float() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(0.0f));
+    assertEquals("0.0", b.asString());
+  }
+
+  public void testAppend_double() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append(0.0));
+    assertEquals("0.0", b.asString());
+  }
+
+  public void testAppend_String() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((String) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append("foo"));
+    assertSame(b, b.append("bar"));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_StringBuilder() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((StringBuilder) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new StringBuilder("foo")));
+    assertSame(b, b.append(new StringBuilder("bar")));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_StringBuffer() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((StringBuffer) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new StringBuffer("foo")));
+    assertSame(b, b.append(new StringBuffer("bar")));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_Object() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((Object) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new Object() {
+      @Override
+      public String toString() {
+        return "foobar";
+      }
+    }));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_CharSequence() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((CharSequence) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append((CharSequence) "foo"));
+    assertSame(b, b.append((CharSequence) "bar"));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testAppend_SafeHtml() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.append((SafeHtml) null));
+    assertEquals("", b.asString());
+    assertSame(b, b.append(new SafeHtmlString("foo")));
+    assertSame(b, b.append(new SafeHtmlBuilder().append("bar")));
+    assertEquals("foobar", b.asString());
+  }
+
+  public void testHtmlSpecialCharacters() {
+    assertEquals("&amp;", escape("&"));
+    assertEquals("&lt;", escape("<"));
+    assertEquals("&gt;", escape(">"));
+    assertEquals("&quot;", escape("\""));
+    assertEquals("&#39;", escape("'"));
+
+    assertEquals("&amp;", escape('&'));
+    assertEquals("&lt;", escape('<'));
+    assertEquals("&gt;", escape('>'));
+    assertEquals("&quot;", escape('"'));
+    assertEquals("&#39;", escape('\''));
+
+    assertEquals("&lt;b&gt;", escape("<b>"));
+    assertEquals("&amp;lt;b&amp;gt;", escape("&lt;b&gt;"));
+  }
+
+  public void testEntityNbsp() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.nbsp());
+    assertEquals("&nbsp;", b.asString());
+  }
+
+  public void testTagBr() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.br());
+    assertEquals("<br />", b.asString());
+  }
+
+  public void testTagTableTrTd() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openElement("table"));
+    assertSame(b, b.openTr());
+    assertSame(b, b.openTd());
+    assertSame(b, b.append("d<a>ta"));
+    assertSame(b, b.closeTd());
+    assertSame(b, b.closeTr());
+    assertSame(b, b.closeElement("table"));
+    assertEquals("<table><tr><td>d&lt;a&gt;ta</td></tr></table>", b.asString());
+  }
+
+  public void testTagDiv() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openDiv());
+    assertSame(b, b.append("d<a>ta"));
+    assertSame(b, b.closeDiv());
+    assertEquals("<div>d&lt;a&gt;ta</div>", b.asString());
+  }
+
+  public void testTagAnchor() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openAnchor());
+
+    assertEquals("", b.getAttribute("href"));
+    assertSame(b, b.setAttribute("href", "http://here"));
+    assertEquals("http://here", b.getAttribute("href"));
+    assertSame(b, b.setAttribute("href", "d<a>ta"));
+    assertEquals("d<a>ta", b.getAttribute("href"));
+
+    assertEquals("", b.getAttribute("target"));
+    assertSame(b, b.setAttribute("target", null));
+    assertEquals("", b.getAttribute("target"));
+
+    assertSame(b, b.append("go"));
+    assertSame(b, b.closeAnchor());
+    assertEquals("<a href=\"d&lt;a&gt;ta\">go</a>", b.asString());
+  }
+
+  public void testTagHeightWidth() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openElement("img"));
+    assertSame(b, b.setHeight(100));
+    assertSame(b, b.setWidth(42));
+    assertSame(b, b.closeSelf());
+    assertEquals("<img height=\"100\" width=\"42\" />", b.asString());
+  }
+
+  public void testStyleName() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertSame(b, b.openSpan());
+    assertSame(b, b.setStyleName("foo"));
+    assertSame(b, b.addStyleName("bar"));
+    assertSame(b, b.append("d<a>ta"));
+    assertSame(b, b.closeSpan());
+    assertEquals("<span class=\"foo bar\">d&lt;a&gt;ta</span>", b.asString());
+  }
+
+  public void testRejectJavaScript_AnchorHref() {
+    final String href = "javascript:window.close();";
+    try {
+      new SafeHtmlBuilder().openAnchor().setAttribute("href", href);
+      fail("accepted javascript in a href");
+    } catch (RuntimeException e) {
+      assertEquals("javascript unsafe in href: " + href, e.getMessage());
+    }
+  }
+
+  public void testRejectJavaScript_ImgSrc() {
+    final String href = "javascript:window.close();";
+    try {
+      new SafeHtmlBuilder().openElement("img").setAttribute("src", href);
+      fail("accepted javascript in img src");
+    } catch (RuntimeException e) {
+      assertEquals("javascript unsafe in href: " + href, e.getMessage());
+    }
+  }
+
+  public void testRejectJavaScript_FormAction() {
+    final String href = "javascript:window.close();";
+    try {
+      new SafeHtmlBuilder().openElement("form").setAttribute("action", href);
+      fail("accepted javascript in form action");
+    } catch (RuntimeException e) {
+      assertEquals("javascript unsafe in href: " + href, e.getMessage());
+    }
+  }
+
+  private static String escape(final char c) {
+    return new SafeHtmlBuilder().append(c).asString();
+  }
+
+  private static String escape(final String c) {
+    return new SafeHtmlBuilder().append(c).asString();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
new file mode 100644
index 0000000..a9d9450
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
@@ -0,0 +1,58 @@
+// 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.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_LinkifyTest extends TestCase {
+  public void testLinkify_SimpleHttp1() {
+    final SafeHtml o = html("A http://go.here/ B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B", n.asString());
+  }
+
+  public void testLinkify_SimpleHttps2() {
+    final SafeHtml o = html("A https://go.here/ B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B", n.asString());
+  }
+
+  public void testLinkify_Parens1() {
+    final SafeHtml o = html("A (http://go.here/) B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B", n.asString());
+  }
+
+  public void testLinkify_Parens() {
+    final SafeHtml o = html("A http://go.here/#m() B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B", n.asString());
+  }
+
+  public void testLinkify_AngleBrackets1() {
+    final SafeHtml o = html("A <http://go.here/> B");
+    final SafeHtml n = o.linkify();
+    assertNotSame(o, n);
+    assertEquals("A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B", n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
new file mode 100644
index 0000000..d7a3aaf
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
@@ -0,0 +1,119 @@
+// 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.safehtml.client;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class SafeHtml_ReplaceTest extends TestCase {
+  public void testReplaceEmpty() {
+    SafeHtml o = html("A\nissue42\nB");
+    assertSame(o, o.replaceAll(null));
+    assertSame(o, o.replaceAll(Collections.<FindReplace> emptyList()));
+  }
+
+  public void testReplaceOneLink() {
+    SafeHtml o = html("A\nissue 42\nB");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("A\n<a href=\"?42\">issue 42</a>\nB", n.asString());
+  }
+
+  public void testReplaceNoLeadingOrTrailingText() {
+    SafeHtml o = html("issue 42");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("<a href=\"?42\">issue 42</a>", n.asString());
+  }
+
+  public void testReplaceTwoLinks() {
+    SafeHtml o = html("A\nissue 42\nissue 9918\nB");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("A\n"
+        + "<a href=\"?42\">issue 42</a>\n"
+        + "<a href=\"?9918\">issue 9918</a>\n"
+        + "B"
+    , n.asString());
+  }
+
+  public void testReplaceInOrder() {
+    SafeHtml o = html("A\nissue 42\nReally GWTEXPUI-9918 is better\nB");
+    SafeHtml n = o.replaceAll(repls(
+        new RawFindReplace("(GWTEXPUI-(\\d+))",
+            "<a href=\"gwtexpui-bug?$2\">$1</a>"),
+        new RawFindReplace("(issue\\s+(\\d+))",
+            "<a href=\"generic-bug?$2\">$1</a>")));
+    assertNotSame(o, n);
+    assertEquals("A\n"
+        + "<a href=\"generic-bug?42\">issue 42</a>\n"
+        + "Really <a href=\"gwtexpui-bug?9918\">GWTEXPUI-9918</a> is better\n"
+        + "B"
+    , n.asString());
+  }
+
+  public void testReplaceOverlappingAfterFirstChar() {
+    SafeHtml o = html("abcd");
+    RawFindReplace ab = new RawFindReplace("ab", "AB");
+    RawFindReplace bc = new RawFindReplace("bc", "23");
+    RawFindReplace cd = new RawFindReplace("cd", "YZ");
+
+    assertEquals("ABcd", o.replaceAll(repls(ab, bc)).asString());
+    assertEquals("ABcd", o.replaceAll(repls(bc, ab)).asString());
+    assertEquals("ABYZ", o.replaceAll(repls(ab, bc, cd)).asString());
+  }
+
+  public void testReplaceOverlappingAtFirstCharLongestMatch() {
+    SafeHtml o = html("abcd");
+    RawFindReplace ab = new RawFindReplace("ab", "AB");
+    RawFindReplace abc = new RawFindReplace("[^d][^d][^d]", "234");
+
+    assertEquals("ABcd", o.replaceAll(repls(ab, abc)).asString());
+    assertEquals("234d", o.replaceAll(repls(abc, ab)).asString());
+  }
+
+  public void testReplaceOverlappingAtFirstCharFirstMatch() {
+    SafeHtml o = html("abcd");
+    RawFindReplace ab1 = new RawFindReplace("ab", "AB");
+    RawFindReplace ab2 = new RawFindReplace("[^cd][^cd]", "12");
+
+    assertEquals("ABcd", o.replaceAll(repls(ab1, ab2)).asString());
+    assertEquals("12cd", o.replaceAll(repls(ab2, ab1)).asString());
+  }
+
+  public void testFailedSanitization() {
+    SafeHtml o = html("abcd");
+    LinkFindReplace evil = new LinkFindReplace("(b)", "javascript:alert('$1')");
+    LinkFindReplace ok = new LinkFindReplace("(b)", "/$1");
+    assertEquals("abcd", o.replaceAll(repls(evil)).asString());
+    String linked = "a<a href=\"/b\">b</a>cd";
+    assertEquals(linked, o.replaceAll(repls(ok)).asString());
+    assertEquals(linked, o.replaceAll(repls(evil, ok)).asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+
+  private static List<FindReplace> repls(FindReplace... repls) {
+    return Arrays.asList(repls);
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
new file mode 100644
index 0000000..250a1b5
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
@@ -0,0 +1,133 @@
+// 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 "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS 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.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyListTest extends TestCase {
+  private static final String BEGIN_LIST = "<ul class=\"wikiList\">";
+  private static final String END_LIST = "</ul>";
+
+  private static String item(String raw) {
+    return "<li>" + raw + "</li>";
+  }
+
+  public void testBulletList1() {
+    final SafeHtml o = html("A\n\n* line 1\n* 2nd line");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testBulletList2() {
+    final SafeHtml o = html("A\n\n* line 1\n* 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testBulletList3() {
+    final SafeHtml o = html("* line 1\n* 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals(BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testBulletList4() {
+    final SafeHtml o = html("To see this bug, you have to:\n" //
+        + "* Be on IMAP or EAS (not on POP)\n"//
+        + "* Be very unlucky\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>To see this bug, you have to:</p>" //
+        + BEGIN_LIST //
+        + item("Be on IMAP or EAS (not on POP)") //
+        + item("Be very unlucky") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testBulletList5() {
+    final SafeHtml o = html("To see this bug,\n" //
+        + "you have to:\n" //
+        + "* Be on IMAP or EAS (not on POP)\n"//
+        + "* Be very unlucky\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>To see this bug, you have to:</p>" //
+        + BEGIN_LIST //
+        + item("Be on IMAP or EAS (not on POP)") //
+        + item("Be very unlucky") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testDashList1() {
+    final SafeHtml o = html("A\n\n- line 1\n- 2nd line");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+    , n.asString());
+  }
+
+  public void testDashList2() {
+    final SafeHtml o = html("A\n\n- line 1\n- 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testDashList3() {
+    final SafeHtml o = html("- line 1\n- 2nd line\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals(BEGIN_LIST //
+        + item("line 1") //
+        + item("2nd line") //
+        + END_LIST //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
new file mode 100644
index 0000000..cbb315b
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
@@ -0,0 +1,82 @@
+// 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 "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS 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.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyPreformatTest extends TestCase {
+  private static final String B = "<span class=\"wikiPreFormat\">";
+  private static final String E = "</span><br />";
+
+  private static String pre(String raw) {
+    return B + raw + E;
+  }
+
+  public void testPreformat1() {
+    final SafeHtml o = html("A\n\n  This is pre\n  formatted");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>"//
+        + "<p>" //
+        + pre("  This is pre") //
+        + pre("  formatted") //
+        + "</p>" //
+    , n.asString());
+  }
+
+  public void testPreformat2() {
+    final SafeHtml o = html("A\n\n  This is pre\n  formatted\n\nbut this is not");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>" //
+        + "<p>" //
+        + pre("  This is pre") //
+        + pre("  formatted") //
+        + "</p>" //
+        + "<p>but this is not</p>" //
+    , n.asString());
+  }
+
+  public void testPreformat3() {
+    final SafeHtml o = html("A\n\n  Q\n    <R>\n  S\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A</p>" //
+        + "<p>" //
+        + pre("  Q") //
+        + pre("    &lt;R&gt;") //
+        + pre("  S") //
+        + "</p>" //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  public void testPreformat4() {
+    final SafeHtml o = html("  Q\n    <R>\n  S\n\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>" //
+        + pre("  Q") //
+        + pre("    &lt;R&gt;") //
+        + pre("  S") //
+        + "</p>" //
+        + "<p>B</p>" //
+    , n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
new file mode 100644
index 0000000..c9837037
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
@@ -0,0 +1,93 @@
+// 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 "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS 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.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyTest extends TestCase {
+  public void testWikify_OneLine1() {
+    final SafeHtml o = html("A  B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A  B</p>", n.asString());
+  }
+
+  public void testWikify_OneLine2() {
+    final SafeHtml o = html("A  B\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A  B\n</p>", n.asString());
+  }
+
+  public void testWikify_OneParagraph1() {
+    final SafeHtml o = html("A\nB");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A\nB</p>", n.asString());
+  }
+
+  public void testWikify_OneParagraph2() {
+    final SafeHtml o = html("A\nB\n");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A\nB\n</p>", n.asString());
+  }
+
+  public void testWikify_TwoParagraphs() {
+    final SafeHtml o = html("A\nB\n\nC\nD");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A\nB</p><p>C\nD</p>", n.asString());
+  }
+
+  public void testLinkify_SimpleHttp1() {
+    final SafeHtml o = html("A http://go.here/ B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B</p>", n.asString());
+  }
+
+  public void testLinkify_SimpleHttps2() {
+    final SafeHtml o = html("A https://go.here/ B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B</p>", n.asString());
+  }
+
+  public void testLinkify_Parens1() {
+    final SafeHtml o = html("A (http://go.here/) B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B</p>", n.asString());
+  }
+
+  public void testLinkify_Parens() {
+    final SafeHtml o = html("A http://go.here/#m() B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B</p>", n.asString());
+  }
+
+  public void testLinkify_AngleBrackets1() {
+    final SafeHtml o = html("A <http://go.here/> B");
+    final SafeHtml n = o.wikify();
+    assertNotSame(o, n);
+    assertEquals("<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B</p>", n.asString());
+  }
+
+  private static SafeHtml html(String text) {
+    return new SafeHtmlBuilder().append(text).toSafeHtml();
+  }
+}
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
new file mode 100644
index 0000000..dc47d82
--- /dev/null
+++ b/gerrit-gwtui/BUCK
@@ -0,0 +1,96 @@
+include_defs('//gerrit-gwtui/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 = ['//:'],
+)
+
+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/**/*']),
+  deps = [
+    '//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-patch-jgit:client',
+    '//gerrit-prettify:client',
+    '//gerrit-reviewdb:client',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtjsonrpc_src',
+    '//lib:gwtorm',
+    '//lib:jsr305',
+    '//lib/gwt:user',
+    '//lib/jgit:jgit',
+  ],
+  visibility = [
+    '//tools/eclipse:classpath',
+    '//Documentation:licenses.txt',
+  ],
+)
+
+java_test(
+  name = 'ui_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':ui_module',
+    '//lib:junit',
+    '//lib/gwt:dev',
+    '//lib/jgit:jgit',
+  ],
+  source_under_test = [':ui_module'],
+)
diff --git a/gerrit-gwtui/DEFS b/gerrit-gwtui/DEFS
new file mode 100644
index 0000000..6ace5bf
--- /dev/null
+++ b/gerrit-gwtui/DEFS
@@ -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.
+
+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 = 'resources/%s_%s.gwt.xml' % (module_target.replace('.', '/'), ua)
+    genrule(
+      name = '%s_%s_gwtxml_gen' % (name, ua),
+      cmd = 'mkdir -p $(dirname $OUT);echo "%s">$OUT' % xml,
+      srcs = [], 
+      deps = [],
+      out = gwt,
+    )
+    java_library(
+      name = '%s_%s_gwtxml_lib' % (name, ua),
+      resources = [genfile(gwt)],
+      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
index 7f72ea4..db64dc5 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
@@ -40,12 +40,14 @@
     </dependency>
 
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
     </dependency>
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
       <classifier>sources</classifier>
       <type>jar</type>
     </dependency>
@@ -149,53 +151,6 @@
     </dependency>
   </dependencies>
 
-  <profiles>
-    <profile>
-      <id>all</id>
-      <activation>
-        <activeByDefault>true</activeByDefault>
-      </activation>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUI</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>false</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>safari</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>chrome</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>webkit</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>gecko1_8</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-    <profile>
-      <id>firefox</id>
-      <properties>
-        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
-        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
-      </properties>
-    </profile>
-  </profiles>
 
   <build>
     <plugins>
@@ -206,12 +161,11 @@
           <execution>
             <id>optimized</id>
             <configuration>
-              <module>${GerritGwtUI.browserType}</module>
+              <module>com.google.gerrit.GerritGwtUI</module>
               <extraJvmArgs>-Xmx512m</extraJvmArgs>
               <compileReport>${gwt.compileReport}</compileReport>
               <disableClassMetadata>true</disableClassMetadata>
               <disableCastChecking>true</disableCastChecking>
-              <draftCompile>${GerritGwtUI.draftCompile}</draftCompile>
             </configuration>
             <goals>
               <goal>compile</goal>
@@ -221,11 +175,10 @@
             <id>debug</id>
             <configuration>
               <style>PRETTY</style>
-              <module>${GerritGwtUI.browserType}</module>
+              <module>com.google.gerrit.GerritGwtUI</module>
               <extraJvmArgs>-Xmx512m</extraJvmArgs>
               <disableClassMetadata>true</disableClassMetadata>
               <disableRunAsync>true</disableRunAsync>
-              <draftCompile>true</draftCompile>
               <webappDirectory>${project.build.directory}/${project.build.finalName}_dbg</webappDirectory>
             </configuration>
             <goals>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
deleted file mode 100644
index 88bea84..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
+++ /dev/null
@@ -1,20 +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.
--->
-<module rename-to="gerrit_ui">
-  <inherits name='com.google.gerrit.GerritGwtUI'/>
-  <set-property name="user.agent" value="safari" />
-  <set-property name="locale" value="default" />
-</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 f2774c9..1dce6df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
@@ -21,4 +21,13 @@
       <when-property-is name="user.agent" value="ie8"/>
     </any>
   </replace-with>
+
+  <replace-with class="com.google.gerrit.client.Themer.ThemerIE">
+    <when-type-is class="com.google.gerrit.client.Themer" />
+    <any>
+      <when-property-is name="user.agent" value="ie6"/>
+      <when-property-is name="user.agent" value="ie8"/>
+      <when-property-is name="user.agent" value="ie9"/>
+    </any>
+  </replace-with>
 </module>
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 0059723..9fa89e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -14,16 +14,26 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gwt.event.dom.client.ErrorEvent;
-import com.google.gwt.event.dom.client.ErrorHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+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.Timer;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.UIObject;
 
 public class AvatarImage extends Image {
 
+  public AvatarImage() {
+  }
+
   /** A default sized avatar image. */
-  public AvatarImage(Account.Id account) {
+  public AvatarImage(AccountInfo account) {
     this(account, 0);
   }
 
@@ -35,30 +45,67 @@
    *        on the avatar provider. A size <= 0 indicates to let the provider
    *        decide a default size.
    */
-  public AvatarImage(Account.Id account, int size) {
-    super(url(account, size));
+  public AvatarImage(AccountInfo account, int size) {
+    this(account, size, true);
+  }
+
+  /**
+   * An avatar image for the given account using the requested size.
+   *
+   * @param account The account in which we are interested
+   * @param size A requested size. Note that the size can be ignored depending
+   *        on the avatar provider. A size <= 0 indicates to let the provider
+   *        decide a default size.
+   * @param addPopup show avatar popup with user info on hovering over the
+   *        avatar image
+   */
+  public AvatarImage(AccountInfo account, int size, boolean addPopup) {
+    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");
     }
 
-    addErrorHandler(new ErrorHandler() {
+    addLoadHandler(new LoadHandler() {
       @Override
-      public void onError(ErrorEvent event) {
-        // We got a 404, don't bother showing the image. Either the user doesn't
-        // have an avatar or there is no avatar provider plugin installed.
-        setVisible(false);
+      public void onLoad(LoadEvent event) {
+        setVisible(true);
       }
     });
+
+    if (addPopup) {
+      UserPopupPanel userPopup = new UserPopupPanel(account, false, false);
+      PopupHandler popupHandler = new PopupHandler(userPopup, this);
+      addMouseOverHandler(popupHandler);
+      addMouseOutHandler(popupHandler);
+    }
   }
 
-  private static String url(Account.Id id, int size) {
+  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() && id.equals(Gerrit.getUserAccount().getId())) {
+    if (Gerrit.isSignedIn() && email.equals(Gerrit.getUserAccount().getPreferredEmail())) {
       u = "self";
     } else {
-      u = id.toString();
+      u = email;
     }
     RestApi api = new RestApi("/accounts/").id(u).view("avatar");
     if (size > 0) {
@@ -66,4 +113,79 @@
     }
     return api.url();
   }
+
+  private class PopupHandler implements MouseOverHandler, MouseOutHandler {
+    private final UserPopupPanel popup;
+    private final UIObject target;
+
+    private Timer showTimer;
+    private Timer hideTimer;
+
+    public PopupHandler(UserPopupPanel popup, UIObject target) {
+      this.popup = popup;
+      this.target = target;
+
+      popup.addDomHandler(new MouseOverHandler() {
+        @Override
+        public void onMouseOver(MouseOverEvent event) {
+          scheduleShow();
+        }
+      }, MouseOverEvent.getType());
+      popup.addDomHandler(new MouseOutHandler() {
+        @Override
+        public void onMouseOut(MouseOutEvent event) {
+          scheduleHide();
+        }
+      }, MouseOutEvent.getType());
+    }
+
+    @Override
+    public void onMouseOver(MouseOverEvent event) {
+      scheduleShow();
+    }
+
+    @Override
+    public void onMouseOut(MouseOutEvent event) {
+      scheduleHide();
+    }
+
+    private void scheduleShow() {
+      if (hideTimer != null) {
+        hideTimer.cancel();
+        hideTimer = null;
+      }
+      if ((popup.isShowing() && popup.isVisible()) || showTimer != null) {
+        return;
+      }
+      showTimer = new Timer() {
+        @Override
+        public void run() {
+          if (!popup.isShowing() || !popup.isVisible()) {
+            popup.showRelativeTo(target);
+          }
+
+        }
+      };
+      showTimer.schedule(600);
+    }
+
+    private void scheduleHide() {
+      if (showTimer != null) {
+        showTimer.cancel();
+        showTimer = null;
+      }
+      if (!popup.isShowing() || !popup.isVisible() || hideTimer != null) {
+        return;
+      }
+      hideTimer = new Timer() {
+        @Override
+        public void run() {
+          popup.hide();
+        }
+      };
+      hideTimer.schedule(50);
+    }
+  }
+
+
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index ee9fa4f..2e94db7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -124,13 +124,8 @@
     return RelativeDateFormatter.format(dt);
   }
 
-  @Deprecated
-  public static String nameEmail(com.google.gerrit.common.data.AccountInfo acct) {
-    return nameEmail(asInfo(acct));
-  }
-
   /**
-   * Formats an account as an name and an email address.
+   * Formats an account as a name and an email address.
    * <p>
    * Example output:
    * <ul>
@@ -142,7 +137,7 @@
    */
   public static String nameEmail(AccountInfo info) {
     String name = info.name();
-    if (name == null) {
+    if (name == null || name.trim().isEmpty()) {
       name = Gerrit.getConfig().getAnonymousCowardName();
     }
 
@@ -165,11 +160,6 @@
     return name(asInfo(acct));
   }
 
-  @Deprecated
-  public static String name(com.google.gerrit.common.data.AccountInfo acct) {
-    return name(asInfo(acct));
-  }
-
   /**
    * Formats an account name.
    * <p>
@@ -177,7 +167,7 @@
    * returns a longer form that includes the email address.
    */
   public static String name(AccountInfo ai) {
-    if (ai.name() != null) {
+    if (ai.name() != null && !ai.name().trim().isEmpty()) {
       return ai.name();
     }
     String email = ai.email();
@@ -188,7 +178,7 @@
     return nameEmail(ai);
   }
 
-  private static AccountInfo asInfo(Account acct) {
+  public static AccountInfo asInfo(Account acct) {
     if (acct == null) {
       return AccountInfo.create(0, null, null);
     }
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 3804c96..c8f40b2 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
@@ -19,6 +19,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 
 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.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
@@ -30,7 +31,6 @@
 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;
@@ -95,6 +95,7 @@
       GWT.create(GerritResources.class);
   public static final SystemInfoService SYSTEM_SVC;
   public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
+  public static Themer THEMER = GWT.create(Themer.class);
 
   private static String myHost;
   private static GerritConfig myConfig;
@@ -248,6 +249,11 @@
     return myAccount;
   }
 
+  /** @return the currently signed in user's account data; empty account data if no account */
+  public static AccountInfo getUserAccountInfo() {
+    return FormatUtil.asInfo(myAccount);
+  }
+
   /** @return access token to prove user identity during REST API calls. */
   public static String getXGerritAuth() {
     return xGerritAuth;
@@ -427,19 +433,13 @@
     }
   }
 
-  private static void populateBottomMenu(final RootPanel btmmenu) {
+  private static void populateBottomMenu(RootPanel btmmenu, HostPageData hpd) {
     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 {
+    String vs = hpd.version;
+    if (vs == null || vs.isEmpty()) {
       vs = "dev";
     }
 
@@ -532,7 +532,7 @@
 
     applyUserPreferences();
     initHistoryHooks();
-    populateBottomMenu(gBottomMenu);
+    populateBottomMenu(gBottomMenu, hpd);
     refreshMenuBar();
 
     History.addValueChangeHandler(new ValueChangeHandler<String>() {
@@ -552,9 +552,17 @@
     if (signInAnchor != null) {
       signInAnchor.setHref(loginRedirect(token));
     }
+
+    saveDefaultTheme();
     loadPlugins(hpd, token);
   }
 
+  private void saveDefaultTheme() {
+    THEMER.init(Document.get().getElementById("gerrit_sitecss"),
+        Document.get().getElementById("gerrit_header"),
+        Document.get().getElementById("gerrit_footer"));
+  }
+
   private void loadPlugins(HostPageData hpd, final String token) {
     if (hpd.plugins != null) {
       for (final String url : hpd.plugins) {
@@ -680,6 +688,7 @@
       addDocLink(m, C.menuDocumentationSearch(), "user-search.html");
       addDocLink(m, C.menuDocumentationUpload(), "user-upload.html");
       addDocLink(m, C.menuDocumentationAccess(), "access-control.html");
+      addDocLink(m, C.menuDocumentationAPI(), "rest-api.html");
       menuLeft.add(m, C.menuDocumentation());
     }
 
@@ -753,9 +762,9 @@
   }
 
   private static void whoAmI(boolean canLogOut) {
-    Account account = getUserAccount();
-    final CurrentUserPopupPanel userPopup =
-        new CurrentUserPopupPanel(account, canLogOut);
+    AccountInfo account = getUserAccountInfo();
+    final UserPopupPanel userPopup =
+        new UserPopupPanel(account, canLogOut, true);
     final FlowPanel userSummaryPanel = new FlowPanel();
     class PopupHandler implements KeyDownHandler, ClickHandler {
       private void showHidePopup() {
@@ -782,7 +791,7 @@
     final PopupHandler popupHandler = new PopupHandler();
     final InlineLabel l = new InlineLabel(FormatUtil.name(account));
     l.setStyleName(RESOURCES.css().menuBarUserName());
-    final AvatarImage avatar = new AvatarImage(account.getId(), 26);
+    final AvatarImage avatar = new AvatarImage(account, 26, false);
     avatar.setStyleName(RESOURCES.css().menuBarUserNameAvatar());
     userSummaryPanel.setStyleName(RESOURCES.css().menuBarUserNamePanel());
     userSummaryPanel.add(l);
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 0b57bcb..683f058 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
@@ -88,6 +88,7 @@
   String menuDocumentationSearch();
   String menuDocumentationUpload();
   String menuDocumentationAccess();
+  String menuDocumentationAPI();
 
   String searchHint();
   String searchButton();
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 f7033e4..defc7e4 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
@@ -71,6 +71,7 @@
 menuDocumentationSearch = Searching
 menuDocumentationUpload = Uploading
 menuDocumentationAccess = Access Controls
+menuDocumentationAPI = REST API
 
 searchHint = Change #, SHA-1, tr:id or owner:email
 searchButton = Search
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 489ff00..a33556e 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
@@ -21,6 +21,7 @@
   String accountContactPrivacyDetails();
   String accountDashboard();
   String accountInfoBlock();
+  String accountLinkPanel();
   String accountName();
   String accountPassword();
   String accountUsername();
@@ -34,6 +35,7 @@
   String approvalhint();
   String approvalrole();
   String approvalscore();
+  String avatarInfoPanel();
   String blockHeader();
   String bottomheader();
   String cAPPROVAL();
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 fc7ea53..098cc77 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
@@ -51,4 +51,10 @@
 
   @Source("addFileComment.png")
   public ImageResource addFileComment();
+
+  @Source("diffy.png")
+  public ImageResource gerritAvatar();
+
+  @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 5f62a52..ec97e58 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
@@ -35,6 +35,16 @@
     type = link.type;
   }
 
+  /**
+   * Can we link to a patch set if it's a draft
+   *
+   * @param ps Patch set to check draft status
+   * @return true if it's not a draft, or we can link to drafts
+   */
+  public boolean canLink(final PatchSet ps) {
+    return !ps.isDraft() || type.getLinkDrafts();
+  }
+
   public String getLinkName() {
     return "(" + type.getLinkName() + ")";
   }
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 2ae6005..46b7d4d 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
@@ -18,9 +18,6 @@
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.animation.client.Animation;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -36,40 +33,11 @@
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 class SearchPanel extends Composite {
-  private static final int FULL_SIZE = 70;
-  private static final int SMALL_SIZE = 45;
-
-  private class SizeAnimation extends Animation {
-    int targetSize;
-    int startSize;
-    public void run(boolean expand) {
-      if(expand) {
-        targetSize = FULL_SIZE;
-        startSize = SMALL_SIZE;
-      } else {
-        targetSize = SMALL_SIZE;
-        startSize = FULL_SIZE;
-      }
-      super.run(300);
-    }
-    @Override
-    protected void onUpdate(double progress) {
-      int size = (int) (targetSize * progress + startSize * (1-progress));
-      searchBox.setVisibleLength(size);
-    }
-
-    @Override
-    protected void onComplete() {
-      searchBox.setVisibleLength(targetSize);
-    }
-  }
   private final HintTextBox searchBox;
   private HandlerRegistration regFocus;
-  private final SizeAnimation sizeAnimation;
 
   SearchPanel() {
     final FlowPanel body = new FlowPanel();
-    sizeAnimation = new SizeAnimation();
     initWidget(body);
     setStyleName(Gerrit.RESOURCES.css().searchPanel());
 
@@ -78,9 +46,6 @@
     searchBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(final KeyPressEvent event) {
-        if (searchBox.getVisibleLength() == SMALL_SIZE) {
-          sizeAnimation.run(true);
-        }
         if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
           if (!suggestionDisplay.isSuggestionSelected) {
             doSearch();
@@ -88,19 +53,11 @@
         }
       }
     });
-    searchBox.addBlurHandler(new BlurHandler() {
-      @Override
-      public void onBlur(BlurEvent event) {
-        if (searchBox.getVisibleLength() != SMALL_SIZE) {
-          sizeAnimation.run(false);
-        }
-      }
-    });
 
     final SuggestBox suggestBox =
         new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
     searchBox.setStyleName("gwt-TextBox");
-    searchBox.setVisibleLength(SMALL_SIZE);
+    searchBox.setVisibleLength(70);
     searchBox.setHintText(Gerrit.C.searchHint());
 
     final Button searchButton = new Button(Gerrit.C.searchButton());
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
new file mode 100644
index 0000000..a532209
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.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.git;
+
+package com.google.gerrit.client;
+
+import com.google.gerrit.client.projects.ThemeInfo;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.StyleElement;
+
+public class Themer {
+  public static class ThemerIE extends Themer {
+    protected ThemerIE() {
+    }
+
+    @Override
+    protected String getCssText(StyleElement el) {
+      return el.getCssText();
+    }
+
+    @Override
+    protected void setCssText(StyleElement el, String css) {
+      el.setCssText(css);
+    }
+  }
+
+  protected StyleElement cssElement;
+  protected Element headerElement;
+  protected Element footerElement;
+  protected String cssText;
+  protected String headerHtml;
+  protected String footerHtml;
+
+  protected Themer() {
+  }
+
+  public void set(ThemeInfo theme) {
+    if (theme != null) {
+      set(theme.css() != null ? theme.css() : cssText,
+          theme.header() != null ? theme.header() : headerHtml,
+          theme.footer() != null ? theme.footer() : footerHtml);
+    } else {
+      set(cssText, headerHtml, footerHtml);
+    }
+  }
+
+  public void clear() {
+    set(null);
+  }
+
+  void init(Element css, Element header, Element footer) {
+    cssElement = StyleElement.as(css);
+    headerElement = header;
+    footerElement = footer;
+
+    cssText = getCssText(this.cssElement);
+    headerHtml = header.getInnerHTML();
+    footerHtml = footer.getInnerHTML();
+  }
+
+  protected String getCssText(StyleElement el) {
+    return el.getInnerHTML();
+  }
+
+  protected void setCssText(StyleElement el, String css) {
+    el.setInnerHTML(css);
+  }
+
+  private void set(String css, String header, String footer) {
+    setCssText(cssElement, css);
+    headerElement.setInnerHTML(header);
+    footerElement.setInnerHTML(footer);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
similarity index 74%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 7983f9c..01811a6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -24,8 +24,8 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 
-public class CurrentUserPopupPanel extends PluginSafePopupPanel {
-  interface Binder extends UiBinder<Widget, CurrentUserPopupPanel> {
+public class UserPopupPanel extends PluginSafePopupPanel {
+  interface Binder extends UiBinder<Widget, UserPopupPanel> {
   }
 
   private static final Binder binder = GWT.create(Binder.class);
@@ -41,9 +41,10 @@
   @UiField
   Anchor settings;
 
-  public CurrentUserPopupPanel(Account account, boolean canLogOut) {
+  public UserPopupPanel(AccountInfo account, boolean canLogOut,
+      boolean showSettingsLink) {
     super(/* auto hide */true, /* modal */false);
-    avatar = new AvatarImage(account.getId(), 100);
+    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
@@ -51,17 +52,21 @@
     show();
     setVisible(false);
     setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
-    if (account.getFullName() != null) {
-      userName.setText(account.getFullName());
+    if (account.name() != null) {
+      userName.setText(account.name());
     }
-    if (account.getPreferredEmail() != null) {
-      userEmail.setText(account.getPreferredEmail());
+    if (account.email() != null) {
+      userEmail.setText(account.email());
     }
     if (canLogOut) {
       logout.setHref(Gerrit.selfRedirect("/logout"));
     } else {
       logout.setVisible(false);
     }
-    settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+    if (showSettingsLink) {
+      settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+    } else {
+      settings.setVisible(false);
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
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 061cf3b..917c078 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
@@ -19,6 +19,7 @@
 public interface AccountConstants extends Constants {
   String settingsHeading();
 
+  String changeAvatar();
   String fullName();
   String preferredEmail();
   String registeredOn();
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 516e959..7175b6a 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
@@ -1,5 +1,6 @@
 settingsHeading = Settings
 
+changeAvatar = Change Avatar
 fullName = Full Name
 preferredEmail = Email Address
 registeredOn = Registered
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 cba2f0b..f35bd4b 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
@@ -348,7 +348,7 @@
 
   void doSave(final AsyncCallback<Account> onSave) {
     String newName = canEditFullName() ? nameTxt.getText() : null;
-    if ("".equals(newName)) {
+    if (newName != null && newName.trim().isEmpty()) {
       newName = null;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
index ae04e0a..01d6e3c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -16,13 +16,23 @@
 
 import static com.google.gerrit.client.FormatUtil.mediumFormat;
 
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.i18n.client.LocaleInfo;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
 
 public class MyProfileScreen extends SettingsScreen {
+  private AvatarImage avatar;
+  private Anchor changeAvatar;
   private int labelIdx, fieldIdx;
   private Grid info;
 
@@ -30,6 +40,18 @@
   protected void onInitUI() {
     super.onInitUI();
 
+    HorizontalPanel h = new HorizontalPanel();
+    add(h);
+
+    VerticalPanel v = new VerticalPanel();
+    v.addStyleName(Gerrit.RESOURCES.css().avatarInfoPanel());
+    h.add(v);
+    avatar = new AvatarImage();
+    v.add(avatar);
+    changeAvatar = new Anchor(Util.C.changeAvatar(), "", "_blank");
+    changeAvatar.setVisible(false);
+    v.add(changeAvatar);
+
     if (LocaleInfo.getCurrentLocale().isRTL()) {
       labelIdx = 1;
       fieldIdx = 0;
@@ -41,7 +63,7 @@
     info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
     info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-    add(info);
+    h.add(info);
 
     int row = 0;
     if (Gerrit.getConfig().siteHasUsernames()) {
@@ -72,6 +94,20 @@
   }
 
   void display(final Account account) {
+    avatar.setAccount(FormatUtil.asInfo(account), 93, false);
+    new RestApi("/accounts/").id("self").view("avatar.change.url")
+        .get(new AsyncCallback<NativeString>() {
+          @Override
+          public void onSuccess(NativeString changeUrl) {
+            changeAvatar.setHref(changeUrl.asString());
+            changeAvatar.setVisible(true);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+
     int row = 0;
     if (Gerrit.getConfig().siteHasUsernames()) {
       info.setWidget(row++, fieldIdx, new UsernameField());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 13de8e7..a528912 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -196,7 +196,7 @@
   }
 
   protected void doAddNew() {
-    final String projectName = nameTxt.getText();
+    final String projectName = nameTxt.getText().trim();
     if ("".equals(projectName)) {
       return;
     }
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 c276a89..51ff978 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
@@ -23,7 +23,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
@@ -318,7 +318,7 @@
       CheckBox checkBox = new CheckBox();
       table.setWidget(row, 1, checkBox);
       checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, new AccountLink(i));
+      table.setWidget(row, 2, new AccountLinkPanel(i));
       table.setText(row, 3, i.email());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
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 1637919..ce27780 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
@@ -158,6 +158,7 @@
   queryLimit, \
   runGC, \
   startReplication, \
+  streamEvents, \
   viewCaches, \
   viewConnections, \
   viewQueue
@@ -173,6 +174,7 @@
 queryLimit = Query Limit
 runGC = Run Garbage Collection
 startReplication = Start Replication
+streamEvents = Stream Events
 viewCaches = View Caches
 viewConnections = View Connections
 viewQueue = View Queue
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
index 53fb3ab..69dff5c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
@@ -27,11 +27,14 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
+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.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.user.client.Event;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.VerticalPanel;
@@ -74,7 +77,24 @@
     addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
     addPanel.add(new SmallHeading(Util.C.headingCreateGroup()));
 
-    addTxt = new NpTextBox();
+    addTxt = new NpTextBox() {
+      @Override
+      public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (event.getTypeInt() == Event.ONPASTE) {
+          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+            @Override
+            public void execute() {
+              if (addTxt.getValue().trim().length() != 0) {
+                addNew.setEnabled(true);
+              }
+            }
+          });
+        }
+      }
+    };
+    addTxt.sinkEvents(Event.ONPASTE);
+
     addTxt.setVisibleLength(60);
     addTxt.addKeyPressHandler(new KeyPressHandler() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index f7d50f2..7749e9c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.ProjectListPopup;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
 import com.google.gerrit.client.ui.ProjectsTable;
@@ -34,11 +35,14 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.reviewdb.client.Project;
+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.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.user.client.Event;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
@@ -110,8 +114,8 @@
     final VerticalPanel fp = new VerticalPanel();
     fp.setStyleName(Gerrit.RESOURCES.css().createProjectPanel());
 
-    initCreateTxt();
     initCreateButton();
+    initCreateTxt();
     initParentBox();
 
     addGrid(fp);
@@ -129,7 +133,23 @@
   }
 
   private void initCreateTxt() {
-    project = new NpTextBox();
+    project = new NpTextBox() {
+      @Override
+      public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (event.getTypeInt() == Event.ONPASTE) {
+          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+            @Override
+            public void execute() {
+              if (project.getValue().trim().length() != 0) {
+                create.setEnabled(true);
+              }
+            }
+          });
+        }
+      }
+    };
+    project.sinkEvents(Event.ONPASTE);
     project.setVisibleLength(50);
     project.addKeyPressHandler(new KeyPressHandler() {
       @Override
@@ -139,10 +159,12 @@
         }
       }
     });
+    new OnEditEnabler(create, project);
   }
 
   private void initCreateButton() {
     create = new Button(Util.C.buttonCreateProject());
+    create.setEnabled(false);
     create.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
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 07f25f4..dac0b6a 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
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.groups.GroupMap;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
@@ -54,6 +54,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    display();
     refresh();
   }
 
@@ -62,9 +63,9 @@
         : ADMIN_GROUPS + "?filter=" + URL.encodeQueryString(subname));
     GroupMap.match(subname,
         new IgnoreOutdatedFilterResultsCallbackWrapper<GroupMap>(this,
-            new ScreenLoadCallback<GroupMap>(this) {
+            new GerritCallback<GroupMap>() {
               @Override
-              protected void preDisplay(final GroupMap result) {
+              public void onSuccess(GroupMap result) {
                 groups.display(result, subname);
                 groups.finishDisplay();
               }
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..23b4c93 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,12 +19,13 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
+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.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;
@@ -165,13 +166,13 @@
   }
 
   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 +186,29 @@
     }
 
     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 result) {
             addBranch.setEnabled(true);
-            super.onFailure(caught);
+            nameTxtBox.setText("");
+            irevTxtBox.setText("");
+            Util.PROJECT_SVC.listBranches(getProjectKey(),
+                new GerritCallback<ListBranchesResult>() {
+                  @Override
+                  public void onSuccess(ListBranchesResult result) {
+                    display(result.getBranches());
+                  }
+                });
           }
-        });
+
+      @Override
+      public void onFailure(Throwable caught) {
+        addBranch.setEnabled(true);
+        selectAllAndFocus(nameTxtBox);
+        new ErrorDialog(caught.getMessage()).center();
+      }
+    });
   }
 
   private static void selectAllAndFocus(final TextBox textBox) {
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 6af437c..ee58420 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
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
@@ -63,6 +63,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    display();
     refresh();
   }
 
@@ -71,9 +72,9 @@
         : ADMIN_PROJECTS + "?filter=" + URL.encodeQueryString(subname));
     ProjectMap.match(subname,
         new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
-            new ScreenLoadCallback<ProjectMap>(this) {
+            new GerritCallback<ProjectMap>() {
               @Override
-              protected void preDisplay(final ProjectMap result) {
+              public void onSuccess(ProjectMap result) {
                 projects.display(result);
               }
             }));
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 0e7bd21..08877d9 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
@@ -27,12 +27,13 @@
 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.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.ReviewerSuggestOracle;
 import com.google.gerrit.common.data.ApprovalDetail;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
+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.event.dom.client.ClickEvent;
@@ -154,6 +155,7 @@
     }
 
     if (Gerrit.getConfig().testChangeMerge()
+        && change.status() != Change.Status.MERGED
         && !change.mergeable()) {
       addMissingLabel(Util.C.messageNeedsRebaseOrHasDependency());
     }
@@ -327,7 +329,7 @@
     final CellFormatter fmt = table.getCellFormatter();
     int col = 0;
 
-    table.setWidget(row, col++, new AccountLink(account));
+    table.setWidget(row, col++, new AccountLinkPanel(account));
     rows.put(account._account_id(), row);
 
     if (ad.canRemove()) {
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..9375e44 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
@@ -82,6 +82,14 @@
   }
 
   /** 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);
+  }
+
+  /** 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);
@@ -100,6 +108,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; }-*/;
 
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 62d85c4..33cdfbb 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
@@ -37,6 +37,7 @@
   String allMergedChanges();
 
   String changeTableColumnSubject();
+  String changeTableColumnStatus();
   String changeTableColumnOwner();
   String changeTableColumnReviewers();
   String changeTableColumnProject();
@@ -123,6 +124,7 @@
   String patchSetInfoCommitter();
   String patchSetInfoDownload();
   String patchSetInfoParents();
+  String patchSetWithDraftCommentsToolTip();
   String initialCommit();
 
   String buttonRebaseChange();
@@ -136,11 +138,17 @@
   String editCommitMessageToolTip();
   String titleEditCommitMessage();
 
+  String buttonCherryPickChangeBegin();
+  String buttonCherryPickChangeSend();
+  String headingCherryPickBranch();
+  String cherryPickCommitMessage();
+  String cherryPickTitle();
+
   String buttonAbandonChangeBegin();
   String buttonAbandonChangeSend();
   String headingAbandonMessage();
   String abandonChangeTitle();
-  String oldVersionHistory();
+  String referenceVersion();
   String baseDiffItem();
   String autoMerge();
 
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 9388c13..f5eb04b 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
@@ -17,6 +17,7 @@
 allMergedChanges = All merged changes
 
 changeTableColumnSubject = Subject
+changeTableColumnStatus = Status
 changeTableColumnOwner = Owner
 changeTableColumnReviewers = Reviewers
 changeTableColumnProject = Project
@@ -100,13 +101,14 @@
 patchSetInfoCommitter = Committer
 patchSetInfoDownload = Download
 patchSetInfoParents = Parent(s)
+patchSetWithDraftCommentsToolTip = Draft comment(s) inside
 initialCommit = Initial Commit
 
 buttonAbandonChangeBegin = Abandon Change
 buttonAbandonChangeSend = Abandon Change
 headingAbandonMessage = Abandon Message:
 abandonChangeTitle = Code Review - Abandon Change
-oldVersionHistory = Old Version History:
+referenceVersion = Reference Version:
 baseDiffItem = Base
 autoMerge = Auto Merge
 
@@ -121,6 +123,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:
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 5cd6cdb..e5c8dcf 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Change;
@@ -37,10 +38,11 @@
   }
 
   public void display(Change chg, Boolean starred, Boolean canEditCommitMessage,
-      PatchSetInfo info,
-      final AccountInfoCache acc, SubmitTypeRecord submitTypeRecord) {
+      PatchSetInfo info, AccountInfoCache acc,
+      SubmitTypeRecord submitTypeRecord,
+      CommentLinkProcessor commentLinkProcessor) {
     infoBlock.display(chg, acc, submitTypeRecord);
     messageBlock.display(chg.currentPatchSetId(), starred,
-      canEditCommitMessage,  info.getMessage());
+        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 2e1b972..00693d9 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
@@ -89,6 +89,8 @@
   private final native NativeMap<LabelInfo> labels0() /*-{ return this.labels; }-*/;
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
 
+  public final native boolean has_permitted_labels()
+  /*-{ return this.hasOwnProperty('permitted_labels') }-*/;
   private final native NativeMap<JavaScriptObject> _permitted_labels()
   /*-{ return this.permitted_labels; }-*/;
   public final Set<String> permitted_labels() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index e4dbc32..b942824 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.CommentedActionDialog;
 import com.google.gerrit.client.ui.InlineHyperlink;
@@ -104,7 +104,7 @@
     changeIdLabel.setPreviewText(chg.getKey().get());
     table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
 
-    table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
+    table.setWidget(R_OWNER, 1, AccountLinkPanel.link(acc, chg.getOwner()));
 
     final FlowPanel p = new FlowPanel();
     p.add(new ProjectSearchLink(chg.getProject()));
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..742454e 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);
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 6c99081..f55c545 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.
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 65b1b70..6819086 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
@@ -18,7 +18,10 @@
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
+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.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.ExpandAllCommand;
@@ -79,6 +82,7 @@
   private PatchSetsBlock patchSetsBlock;
 
   private Panel comments;
+  private CommentLinkProcessor commentLinkProcessor;
 
   private KeyCommandSet keysNavigation;
   private KeyCommandSet keysAction;
@@ -226,7 +230,7 @@
 
     patchesGrid = new Grid(1, 2);
     patchesGrid.setStyleName(Gerrit.RESOURCES.css().selectPatchSetOldVersion());
-    patchesGrid.setText(0, 0, Util.C.oldVersionHistory());
+    patchesGrid.setText(0, 0, Util.C.referenceVersion());
     patchesGrid.setWidget(0, 1, patchesList);
     add(patchesGrid);
 
@@ -260,10 +264,26 @@
   @Override
   public void onValueChange(final ValueChangeEvent<ChangeDetail> event) {
     if (isAttached()) {
-      // Until this screen is fully migrated to the new API, this call must be
-      // sequential, because we can't start an async get at the source of every
-      // call that might trigger a value change.
-      ChangeApi.detail(event.getValue().getChange().getId().get(),
+      // Until this screen is fully migrated to the new API, these calls must
+      // happen sequentially after the ChangeDetail lookup, because we can't
+      // start an async get at the source of every call that might trigger a
+      // value change.
+      CallbackGroup cbs = new CallbackGroup();
+      ConfigInfoCache.get(
+          event.getValue().getChange().getProject(),
+          cbs.add(new GerritCallback<ConfigInfoCache.Entry>() {
+            @Override
+            public void onSuccess(ConfigInfoCache.Entry result) {
+              commentLinkProcessor = result.getCommentLinkProcessor();
+              setTheme(result.getTheme());
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              // Handled by last callback's onFailure.
+            }
+          }));
+      ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.add(
           new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
             @Override
             public void onSuccess(
@@ -271,7 +291,7 @@
               changeInfo = result;
               display(event.getValue());
             }
-          });
+          }));
     }
   }
 
@@ -292,7 +312,8 @@
         detail.isStarred(),
         detail.canEditCommitMessage(),
         detail.getCurrentPatchSetDetail().getInfo(),
-        detail.getAccounts(), detail.getSubmitTypeRecord());
+        detail.getAccounts(), detail.getSubmitTypeRecord(),
+        commentLinkProcessor);
     dependsOn.display(detail.getDependsOn());
     neededBy.display(detail.getNeededBy());
     approvals.display(changeInfo);
@@ -411,8 +432,8 @@
         isRecent = msg.getWrittenOn().after(aged);
       }
 
-      final CommentPanel cp =
-          new CommentPanel(author, msg.getWrittenOn(), msg.getMessage());
+      final CommentPanel cp = new CommentPanel(author, msg.getWrittenOn(),
+          msg.getMessage(), commentLinkProcessor);
       cp.setRecent(isRecent);
       cp.addStyleName(Gerrit.RESOURCES.css().commentPanelBorder());
       if (i == msgList.size() - 1) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 97a5a09..3a65d20 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.client.FormatUtil.shortFormat;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -182,8 +182,8 @@
     }
   }
 
-  private AccountLink link(final Account.Id id) {
-    return AccountLink.link(accountCache, id);
+  private AccountLinkPanel link(final Account.Id id) {
+    return AccountLinkPanel.link(accountCache, id);
   }
 
   public void addSection(final Section s) {
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 07f0c11..54dce43 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
@@ -19,7 +19,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -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) {
@@ -191,14 +195,15 @@
     }
 
     String subject = Util.cropSubject(c.subject());
-    Change.Status status = c.status();
-    if (status != Change.Status.NEW) {
-      subject += " (" + Util.toLongString(status) + ")";
-    }
     table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
 
+    Change.Status status = c.status();
+    if (status != Change.Status.NEW) {
+      table.setText(row, C_STATUS, Util.toLongString(status));
+    }
+
     if (c.owner() != null) {
-      table.setWidget(row, C_OWNER, new AccountLink(c.owner(), status));
+      table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
     } else {
       table.setText(row, C_OWNER, "");
     }
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 ea184df..198480e 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
@@ -68,8 +68,9 @@
     initWidget(uiBinder.createAndBindUi(this));
   }
 
-  public void display(final String commitMessage) {
-    display(null, null, false, commitMessage);
+  public void display(String commitMessage,
+      CommentLinkProcessor commentLinkProcessor) {
+    display(null, null, false, commitMessage, commentLinkProcessor);
   }
 
   private abstract class CommitMessageEditDialog extends CommentedActionDialog<ChangeDetail> {
@@ -103,7 +104,8 @@
   }
 
   public void display(final PatchSet.Id patchSetId,
-      Boolean starred, Boolean canEditCommitMessage, final String commitMessage) {
+      Boolean starred, Boolean canEditCommitMessage, final String commitMessage,
+      CommentLinkProcessor commentLinkProcessor) {
     starPanel.clear();
     if (patchSetId != null && starred != null && Gerrit.isSignedIn()) {
       Change.Id changeId = patchSetId.getParentKey();
@@ -170,7 +172,7 @@
     // Linkify commit summary
     SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
     commitSummaryLinkified = commitSummaryLinkified.linkify();
-    commitSummaryLinkified = CommentLinkProcessor.apply(commitSummaryLinkified);
+    commitSummaryLinkified = commentLinkProcessor.apply(commitSummaryLinkified);
     commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
 
     // Hide commit body if there is no body
@@ -180,7 +182,7 @@
       // Linkify commit body
       SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
       commitBodyLinkified = commitBodyLinkified.linkify();
-      commitBodyLinkified = CommentLinkProcessor.apply(commitBodyLinkified);
+      commitBodyLinkified = commentLinkProcessor.apply(commitBodyLinkified);
       commitBodyLinkified = commitBodyLinkified.replaceAll("\n\n", "<p></p>");
       commitBodyLinkified = commitBodyLinkified.replaceAll("\n", "<br />");
       commitBodyPre.setInnerHTML(commitBodyLinkified.asString());
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 76f77a2..7baff8a 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.ui.AccountLink;
-import com.google.gerrit.client.ui.CommentedActionDialog;
+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.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,16 +39,19 @@
 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.InlineLabel;
 import com.google.gwt.user.client.ui.Panel;
@@ -78,7 +85,8 @@
    * Creates a closed complex disclosure panel for a patch set.
    * The patch set details are loaded when the complex disclosure panel is opened.
    */
-  public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen) {
+  public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen,
+      boolean hasDraftComments) {
     super(Util.M.patchSetHeader(ps.getPatchSetId()), isOpen);
     detailCache = ChangeCache.get(ps.getId().getParentKey()).getChangeDetailCache();
     changeDetail = detailCache.get();
@@ -87,11 +95,17 @@
     body = new FlowPanel();
     setContent(body);
 
+    if (hasDraftComments) {
+      final Image draftComments = new Image(Gerrit.RESOURCES.draftComments());
+      draftComments.setTitle(Util.C.patchSetWithDraftCommentsToolTip());
+      getHeader().add(draftComments);
+    }
+
     final GitwebLink gw = Gerrit.getGitwebLink();
     final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " ");
     revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
     getHeader().add(revtxt);
-    if (gw != null) {
+    if (gw != null && gw.canLink(ps)) {
       final Anchor revlink =
           new Anchor(gw.getLinkName(), false, gw.toRevision(changeDetail.getChange()
               .getProject(), ps));
@@ -110,6 +124,7 @@
     } else {
       addOpenHandler(this);
     }
+
   }
 
   public void setDiffBaseId(PatchSet.Id diffBaseId) {
@@ -165,6 +180,7 @@
           if (changeDetail.isCurrentPatchSet(detail)) {
             populateActions(detail);
           }
+          populateCommands(detail);
         }
         if (detail.getPatchSet().isDraft()) {
           if (changeDetail.canPublish()) {
@@ -250,7 +266,7 @@
     fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
     if (who.getName() != null) {
       if (who.getAccount() != null) {
-        fp.add(new AccountLink(who));
+        fp.add(new AccountLinkPanel(who));
       } else {
         final InlineLabel lbl = new InlineLabel(who.getName());
         lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
@@ -374,6 +390,48 @@
       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());
+              message.setText(Util.M.cherryPickedChangeDefaultMessage(
+                  detail.getInfo().getMessage().trim(),
+                  detail.getPatchSet().getRevision().get()));
+            }
+
+            @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() {
@@ -489,6 +547,45 @@
     }
   }
 
+  private void populateCommands(final PatchSetDetail detail) {
+    for (final UiCommandDetail cmd : detail.getCommands()) {
+      final Button b = new Button(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());
+                  }
+                }
+              };
+          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() {
@@ -624,25 +721,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/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
index 5a6e427..4805185 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -88,7 +88,8 @@
 
     for (final PatchSet ps : patchSets) {
       final PatchSetComplexDisclosurePanel p =
-          new PatchSetComplexDisclosurePanel(ps, ps == currps);
+          new PatchSetComplexDisclosurePanel(ps, ps == currps,
+              detail.hasDraftComments(ps.getId()));
       if (diffBaseId != null) {
         p.setDiffBaseId(diffBaseId);
         if (ps == currps) {
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 5791f68..d47e2c1 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
@@ -48,7 +48,9 @@
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class PatchTable extends Composite {
   public interface PatchValidator {
@@ -80,6 +82,7 @@
   private String savePointerId;
   private PatchSet.Id base;
   private List<Patch> patchList;
+  private Map<Patch.Key, Integer> patchMap;
   private ListenableAccountDiffPreference listenablePrefs;
 
   private List<ClickHandler> clickHandlers;
@@ -97,18 +100,25 @@
   }
 
   public int indexOf(Patch.Key patch) {
-    for (int i = 0; i < patchList.size(); i++) {
-      if (patchList.get(i).getKey().equals(patch)) {
-        return i;
+    Integer i = patchMap().get(patch);
+    return i != null ? i : -1;
+  }
+
+  private Map<Key, Integer> patchMap() {
+    if (patchMap == null) {
+      patchMap = new HashMap<Patch.Key, Integer>();
+      for (int i = 0; i < patchList.size(); i++) {
+        patchMap.put(patchList.get(i).getKey(), i);
       }
     }
-    return -1;
+    return patchMap;
   }
 
   public void display(PatchSet.Id base, PatchSetDetail detail) {
     this.base = base;
     this.detail = detail;
     this.patchList = detail.getPatches();
+    this.patchMap = null;
     myTable = null;
 
     final DisplayCommand cmd = new DisplayCommand(patchList, base);
@@ -244,8 +254,7 @@
 
     Key thisKey = patch.getKey();
     PatchLink link;
-    if (patchType == PatchScreen.Type.SIDE_BY_SIDE
-        && patch.getPatchType() == Patch.PatchType.UNIFIED) {
+    if (patchType == PatchScreen.Type.SIDE_BY_SIDE) {
       link = new PatchLink.SideBySide("", base, thisKey, index, detail, this);
     } else {
       link = new PatchLink.Unified("", base, thisKey, index, detail, this);
@@ -294,10 +303,6 @@
     return listenablePrefs;
   }
 
-  public void setPreferences(ListenableAccountDiffPreference prefs) {
-    listenablePrefs = prefs;
-  }
-
   private class MyTable extends NavigationTable<Patch> {
     private static final int C_PATH = 2;
     private static final int C_DRAFT = 3;
@@ -331,36 +336,33 @@
     }
 
     void updateReviewedStatus(final Patch.Key patchKey, boolean reviewed) {
-      final int row = findRow(patchKey);
-      if (0 <= row) {
-        final Patch patch = getRowItem(row);
-        if (patch != null) {
-          patch.setReviewedByCurrentUser(reviewed);
-
+      int idx = patchMap().get(patchKey);
+      if (0 <= idx) {
+        Patch patch = patchList.get(idx);
+        if (patch.isReviewedByCurrentUser() != reviewed) {
+          int row = idx + 1;
           int col = C_SIDEBYSIDE + 2;
           if (patch.getPatchType() == Patch.PatchType.BINARY) {
             col = C_SIDEBYSIDE + 3;
           }
-
           if (reviewed) {
             table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
           } else {
             table.clearCell(row, col);
           }
+          patch.setReviewedByCurrentUser(reviewed);
         }
       }
     }
 
     void notifyDraftDelta(final Patch.Key key, final int delta) {
-      final int row = findRow(key);
-      if (0 <= row) {
-        final Patch p = getRowItem(row);
-        if (p != null) {
-          p.setDraftCount(p.getDraftCount() + delta);
-          final SafeHtmlBuilder m = new SafeHtmlBuilder();
-          appendCommentCount(m, p);
-          SafeHtml.set(table, row, C_DRAFT, m);
-        }
+      int idx = patchMap().get(key);
+      if (0 <= idx) {
+        Patch p = patchList.get(idx);
+        p.setDraftCount(p.getDraftCount() + delta);
+        SafeHtmlBuilder m = new SafeHtmlBuilder();
+        appendCommentCount(m, p);
+        SafeHtml.set(table, idx + 1, C_DRAFT, m);
       }
     }
 
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 a79978f..4fa76dd 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.patches.AbstractPatchContentTable;
 import com.google.gerrit.client.patches.CommentEditorContainer;
 import com.google.gerrit.client.patches.CommentEditorPanel;
+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.Natives;
@@ -78,6 +79,7 @@
   private boolean saveStateOnUnload = true;
   private List<CommentEditorPanel> commentEditors;
   private ChangeInfo change;
+  private CommentLinkProcessor commentLinkProcessor;
 
   public PublishCommentScreen(final PatchSet.Id psi) {
     patchSetId = psi;
@@ -148,8 +150,8 @@
     super.onLoad();
 
     CallbackGroup cbs = new CallbackGroup();
-    ChangeApi.revision(patchSetId).view("review").get(cbs.add(
-        new AsyncCallback<ChangeInfo>() {
+    ChangeApi.revision(patchSetId).view("review")
+        .get(cbs.add(new AsyncCallback<ChangeInfo>() {
           @Override
           public void onSuccess(ChangeInfo result) {
             result.init();
@@ -166,7 +168,7 @@
           @Override
           protected void preDisplay(final PatchSetPublishDetail result) {
             send.setEnabled(true);
-            display(result);
+            PublishCommentScreen.this.preDisplay(result, this);
           }
 
           @Override
@@ -176,6 +178,24 @@
         }));
   }
 
+  private void preDisplay(final PatchSetPublishDetail pubDetail,
+      final ScreenLoadCallback<PatchSetPublishDetail> origCb) {
+    ConfigInfoCache.get(pubDetail.getChange().getProject(),
+        new AsyncCallback<ConfigInfoCache.Entry>() {
+          @Override
+          public void onSuccess(ConfigInfoCache.Entry result) {
+            commentLinkProcessor = result.getCommentLinkProcessor();
+            setTheme(result.getTheme());
+            display(pubDetail);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            origCb.onFailure(caught);
+          }
+        });
+  }
+
   @Override
   protected void onUnload() {
     super.onUnload();
@@ -249,6 +269,9 @@
   }
 
   private void initLabel(String labelName, Panel body) {
+    if (!change.has_permitted_labels()) {
+      return;
+    }
     JsArrayString nativeValues = change.permitted_values(labelName);
     if (nativeValues == null || nativeValues.length() == 0) {
       return;
@@ -278,7 +301,7 @@
     for (String value : values) {
       ValueRadioButton b = new ValueRadioButton(label, value);
       SafeHtml buf = new SafeHtmlBuilder().append(b.format());
-      buf = CommentLinkProcessor.apply(buf);
+      buf = commentLinkProcessor.apply(buf);
       SafeHtml.set(b, buf);
 
       if (lastState != null && patchSetId.equals(lastState.patchSetId)
@@ -298,7 +321,7 @@
     setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
         patchSetId.get()));
     descBlock.display(r.getChange(), null, false, r.getPatchSetInfo(), r.getAccounts(),
-        r.getSubmitTypeRecord());
+       r.getSubmitTypeRecord(), commentLinkProcessor);
 
     if (r.getChange().getStatus().isOpen()) {
       initApprovals(approvalPanel);
@@ -334,11 +357,14 @@
           priorFile = fn;
         }
 
-        final CommentEditorPanel editor = new CommentEditorPanel(c);
+        final CommentEditorPanel editor =
+            new CommentEditorPanel(c, commentLinkProcessor);
         if (c.getLine() == AbstractPatchContentTable.R_HEAD) {
-          editor.setAuthorNameText(Util.C.fileCommentHeader());
+          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
+              Util.C.fileCommentHeader());
         } else {
-          editor.setAuthorNameText(Util.M.lineHeader(c.getLine()));
+          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
+              Util.M.lineHeader(c.getLine()));
         }
         editor.setOpen(true);
         commentEditors.add(editor);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java
index d83bad9..b02f0474 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java
@@ -18,6 +18,7 @@
 
 public interface DashboardConstants extends Constants {
   String dashboardName();
+  String dashboardTitle();
   String dashboardDescription();
   String dashboardInherited();
   String dashboardItem();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties
index 4e999b3..ac4de7c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties
@@ -1,4 +1,5 @@
 dashboardName = Dashboard Name
+dashboardTitle = Dashboard Title
 dashboardDescription = Dashboard Description
 dashboardInherited = Inherited From
 dashboardItem = dashboard
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java
index 9eb42f3..44d74ab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java
@@ -18,6 +18,7 @@
 
 public class DashboardInfo extends JavaScriptObject {
   public final native String id() /*-{ return this.id; }-*/;
+  public final native String title() /*-{ return this.title; }-*/;
   public final native String project() /*-{ return this.project; }-*/;
   public final native String definingProject() /*-{ return this.defining_project; }-*/;
   public final native String ref() /*-{ return this.ref; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
index f9df0d7..00721b7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
@@ -47,10 +47,12 @@
     fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
     fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
     fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
 
     table.setText(0, 1, Util.C.dashboardName());
-    table.setText(0, 2, Util.C.dashboardDescription());
-    table.setText(0, 3, Util.C.dashboardInherited());
+    table.setText(0, 2, Util.C.dashboardTitle());
+    table.setText(0, 3, Util.C.dashboardDescription());
+    table.setText(0, 4, Util.C.dashboardInherited());
   }
 
   public void display(DashboardList dashes) {
@@ -99,7 +101,7 @@
     table.setText(row, 0, section);
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setColSpan(row, 0, 5);
+    fmt.setColSpan(row, 0, 6);
     fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().sectionHeader());
   }
 
@@ -113,6 +115,7 @@
     fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
     fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
     fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
 
     populate(row, k);
   }
@@ -125,9 +128,10 @@
     }
     table.setWidget(row, 2, new Anchor(k.path(), "#"
             + PageLinks.toProjectDashboard(new Project.NameKey(k.project()), k.id())));
-    table.setText(row, 3, k.description());
+    table.setText(row, 3, k.title() != null ? k.title() : k.path());
+    table.setText(row, 4, k.description());
     if (k.definingProject() != null && !k.definingProject().equals(k.project())) {
-      table.setWidget(row, 4, new Anchor(k.definingProject(), "#"
+      table.setWidget(row, 5, new Anchor(k.definingProject(), "#"
           + PageLinks.toProjectDashboards(new Project.NameKey(k.definingProject()))));
     }
     setRowItem(row, k);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
new file mode 100644
index 0000000..4be4541
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png
new file mode 100644
index 0000000..31c770f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png
Binary files differ
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 255412d..5f16af6 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
@@ -20,7 +20,7 @@
 
 @def black #000000;
 @def white #ffffff;
-@def norm-font  Arial Unicode MS, Arial, sans-serif;
+@def norm-font  sans-serif;
 @def mono-font  monospace;
 
 @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
@@ -86,6 +86,21 @@
   text-decoration: underline;
 }
 
+.accountLinkPanel {
+  display: inline;
+}
+
+.accountLinkPanel img {
+  margin-right: 0.2em;
+  position: relative;
+  top: 2px;
+}
+
+.accountLinkPanel a {
+  position: relative;
+  top: -1px;
+}
+
 .accountName {
   white-space: nowrap;
 }
@@ -514,6 +529,8 @@
   padding-right: 5px;
   border-right: 1px solid trimColor;
   border-bottom: 1px solid trimColor;
+  vertical-align: middle;
+  height: 20px;
 }
 
 .changeTable a.gwt-InlineHyperlink {
@@ -920,7 +937,6 @@
 }
 
 .sideBySideTableBinaryHeader {
-  border-right: thin solid #b0bdcc;
   border-left:  thin solid #b0bdcc;
   width: 100%;
   color: grey;
@@ -965,6 +981,13 @@
   float: left;
 }
 
+.avatarInfoPanel {
+  margin-right: 10px;
+}
+.avatarInfoPanel td {
+  text-align: center;
+}
+
 .infoBlock {
   border-collapse: collapse;
   border-spacing: 0;
@@ -1158,7 +1181,7 @@
   margin-right: 5em;
   font-weight: bold;
   font-size: medium;
-  font-family: Arial Unicode;
+  font-family: norm-font;
 }
 
 /** Patch History Table **/
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 2f8d135..a0789ec 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
@@ -29,13 +30,14 @@
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.common.PrettyFormatter;
+import com.google.gerrit.prettify.client.PrettyFormatter;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -60,6 +62,9 @@
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtorm.client.KeyUtil;
+
+import org.eclipse.jgit.diff.Edit;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -84,6 +89,7 @@
   private HandlerRegistration regComment;
   private final KeyCommandSet keysOpenByEnter;
   private HandlerRegistration regOpenByEnter;
+  private CommentLinkProcessor commentLinkProcessor;
   boolean isDisplayBinary;
 
   protected AbstractPatchContentTable() {
@@ -239,13 +245,31 @@
     render(s, d);
   }
 
-  protected boolean hasDifferences(final PatchScript script) {
-    // True if there are differences between the two patch sets
-    boolean hasEdits = !script.getEdits().isEmpty();
-    // True if this change is a mode change or a pure rename/copy
-    boolean hasMeta = !script.getPatchHeader().isEmpty();
+  void setCommentLinkProcessor(CommentLinkProcessor commentLinkProcessor) {
+    this.commentLinkProcessor = commentLinkProcessor;
+  }
 
-    return hasEdits || hasMeta;
+  protected boolean hasDifferences(PatchScript script) {
+    return hasEdits(script) || hasMeta(script);
+  }
+
+  public boolean isPureMetaChange(PatchScript script) {
+    return !hasEdits(script) && hasMeta(script);
+  }
+
+  // True if there are differences between the two patch sets
+  private boolean hasEdits(PatchScript script) {
+    for (Edit e : script.getEdits()) {
+      if (e.getType() != Edit.Type.EMPTY) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // True if this change is a mode change or a pure rename/copy
+  private boolean hasMeta(PatchScript script) {
+    return !script.getPatchHeader().isEmpty();
   }
 
   protected void appendNoDifferences(SafeHtmlBuilder m) {
@@ -276,20 +300,37 @@
   protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) {
     AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
 
+    SparseFileContent b = s.getB();
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
     f.setDiffPrefs(dp);
-    f.setFileName(s.getB().getPath());
+    f.setFileName(b.getPath());
     f.setEditFilter(PrettyFormatter.B);
     f.setEditList(s.getEdits());
 
-    if (dp.isSyntaxHighlighting() && s.getA().isWholeFile() && !s.getB().isWholeFile()) {
-      f.format(s.getB().apply(s.getA(), s.getEdits()));
-    } else {
-      f.format(s.getB());
+    if (s.getA().isWholeFile() && !b.isWholeFile()) {
+      b = b.apply(s.getA(), s.getEdits());
     }
+    f.format(b);
     return f;
   }
 
+  protected String getUrlA() {
+    final String rawBase = GWT.getHostPageBaseURL() + "cat/";
+    final String url;
+    if (idSideA == null) {
+      url = rawBase + KeyUtil.encode(patchKey.toString()) + "^1";
+    } else {
+      Patch.Key k = new Patch.Key(idSideA, patchKey.get());
+      url = rawBase + KeyUtil.encode(k.toString()) + "^0";
+    }
+    return url;
+  }
+
+  protected String getUrlB() {
+    final String rawBase = GWT.getHostPageBaseURL() + "cat/";
+    return rawBase + KeyUtil.encode(patchKey.toString()) + "^0";
+  }
+
   protected abstract void render(PatchScript script, final PatchSetDetail detail);
 
   protected abstract void onInsertComment(PatchLine pl);
@@ -537,7 +578,8 @@
       return null;
     }
 
-    final CommentEditorPanel ed = new CommentEditorPanel(newComment);
+    final CommentEditorPanel ed =
+        new CommentEditorPanel(newComment, commentLinkProcessor);
     ed.addFocusHandler(this);
     ed.addBlurHandler(this);
     boolean isCommentRow = false;
@@ -674,7 +716,8 @@
   protected void bindComment(final int row, final int col,
       final PatchLineComment line, final boolean isLast, boolean expandComment) {
     if (line.getStatus() == PatchLineComment.Status.DRAFT) {
-      final CommentEditorPanel plc = new CommentEditorPanel(line);
+      final CommentEditorPanel plc =
+          new CommentEditorPanel(line, commentLinkProcessor);
       plc.addFocusHandler(this);
       plc.addBlurHandler(this);
       table.setWidget(row, col, plc);
@@ -848,7 +891,7 @@
     final Button replyDone;
 
     PublishedCommentPanel(final AccountInfo author, final PatchLineComment c) {
-      super(author, c.getWrittenOn(), c.getMessage());
+      super(author, c.getWrittenOn(), c.getMessage(), commentLinkProcessor);
       this.comment = c;
 
       reply = new Button(PatchUtil.C.buttonReply());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index b609f15..9ed3f18 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -16,20 +16,20 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 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.KeyDownHandler;
 import com.google.gwt.user.client.Timer;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.sql.Timestamp;
@@ -59,11 +59,13 @@
   private final Button discard;
   private final Timer expandTimer;
 
-  public CommentEditorPanel(final PatchLineComment plc) {
+  public CommentEditorPanel(final PatchLineComment plc,
+      final CommentLinkProcessor commentLinkProcessor) {
+    super(commentLinkProcessor);
     comment = plc;
 
     addStyleName(Gerrit.RESOURCES.css().commentEditorPanel());
-    setAuthorNameText(PatchUtil.C.draft());
+    setAuthorNameText(Gerrit.getUserAccountInfo(), PatchUtil.C.draft());
     setMessageText(plc.getMessage());
     addDoubleClickHandler(this);
 
@@ -81,18 +83,6 @@
     text.addKeyDownHandler(new KeyDownHandler() {
       @Override
       public void onKeyDown(final KeyDownEvent event) {
-        if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE
-            && !event.isAnyModifierKeyDown()) {
-          event.preventDefault();
-
-          if (isNew()) {
-            onDiscard();
-          } else {
-            render();
-          }
-          return;
-        }
-
         if ((event.isControlKeyDown() || event.isMetaKeyDown())
             && !event.isAltKeyDown() && !event.isShiftKeyDown()) {
           switch (event.getNativeKeyCode()) {
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 9d4a481..8c9c56b 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
@@ -36,6 +36,7 @@
   String patchHistoryTitle();
   String disabledOnLargeFiles();
   String intralineFailure();
+  String intralineTimeout();
   String illegalNumberOfColumns();
 
   String upToChange();
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 e9fec4b..5acdb5f 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
@@ -18,6 +18,7 @@
 patchSet = Patch Set
 disabledOnLargeFiles = Disabled on very large source files.
 intralineFailure = Intraline difference not available due to server error.
+intralineTimeout = Intraline difference not available due to timeout.
 illegalNumberOfColumns = The number of columns cannot be zero or negative
 
 upToChange = Up to change
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 402f939..4e24742 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
@@ -21,14 +21,17 @@
 import com.google.gerrit.client.changes.CommitMessageBlock;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
+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.ScreenLoadCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.common.PrettyFactory;
+import com.google.gerrit.prettify.client.PrettyFactory;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -38,6 +41,7 @@
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
+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;
@@ -97,6 +101,7 @@
   protected PatchSet.Id idSideB;
   protected PatchScriptSettingsPanel settingsPanel;
   protected TopView topView;
+  protected CommentLinkProcessor commentLinkProcessor;
 
   private ReviewedPanels reviewedPanels;
   private HistoryTable historyTable;
@@ -113,6 +118,7 @@
   /** The index of the file we are currently looking at among the fileList */
   private int patchIndex;
   private ListenableAccountDiffPreference prefs;
+  private HandlerRegistration prefsHandler;
 
   /** Keys that cause an action on this screen */
   private KeyCommandSet keysNavigation;
@@ -120,6 +126,7 @@
   private HandlerRegistration regNavigation;
   private HandlerRegistration regAction;
   private boolean intralineFailure;
+  private boolean intralineTimeout;
 
   /**
    * How this patch should be displayed in the patch screen.
@@ -140,19 +147,12 @@
     idSideB = id.getParentKey();
     this.patchIndex = patchIndex;
 
-    prefs = fileList != null ? fileList.getPreferences() :
-                               new ListenableAccountDiffPreference();
+    prefs = fileList != null
+        ? fileList.getPreferences()
+        : new ListenableAccountDiffPreference();
     if (Gerrit.isSignedIn()) {
       prefs.reset();
     }
-    prefs.addValueChangeHandler(
-        new ValueChangeHandler<AccountDiffPreference>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
-            update(event.getValue());
-          }
-        });
-
     reviewedPanels = new ReviewedPanels();
     settingsPanel = new PatchScriptSettingsPanel(prefs);
   }
@@ -300,6 +300,10 @@
 
   @Override
   protected void onUnload() {
+    if (prefsHandler != null) {
+      prefsHandler.removeHandler();
+      prefsHandler = null;
+    }
     if (regNavigation != null) {
       regNavigation.removeHandler();
       regNavigation = null;
@@ -365,23 +369,40 @@
     if (isFirst && fileList != null) {
       fileList.movePointerTo(patchKey);
     }
-    PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB, //
-        settingsPanel.getValue(), new ScreenLoadCallback<PatchScript>(this) {
+
+    CallbackGroup cb = new CallbackGroup();
+    ConfigInfoCache.get(patchSetDetail.getProject(),
+        cb.add(new AsyncCallback<ConfigInfoCache.Entry>() {
           @Override
-          protected void preDisplay(final PatchScript result) {
-            if (rpcSequence == rpcseq) {
-              onResult(result, isFirst);
-            }
+          public void onSuccess(ConfigInfoCache.Entry result) {
+            commentLinkProcessor = result.getCommentLinkProcessor();
+            contentTable.setCommentLinkProcessor(commentLinkProcessor);
+            setTheme(result.getTheme());
           }
 
           @Override
-          public void onFailure(final Throwable caught) {
-            if (rpcSequence == rpcseq) {
-              settingsPanel.setEnabled(true);
-              super.onFailure(caught);
-            }
+          public void onFailure(Throwable caught) {
+            // Handled by ScreenLoadCallback.onFailure.
           }
-        });
+        }));
+    PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB,
+        settingsPanel.getValue(), cb.addGwtjsonrpc(
+            new ScreenLoadCallback<PatchScript>(this) {
+              @Override
+              protected void preDisplay(final PatchScript result) {
+                if (rpcSequence == rpcseq) {
+                  onResult(result, isFirst);
+                }
+              }
+
+              @Override
+              public void onFailure(final Throwable caught) {
+                if (rpcSequence == rpcseq) {
+                  settingsPanel.setEnabled(true);
+                  super.onFailure(caught);
+                }
+              }
+        }));
   }
 
   private void onResult(final PatchScript script, final boolean isFirst) {
@@ -397,7 +418,8 @@
 
     if (idSideB.equals(patchSetDetail.getPatchSet().getId())) {
       commitMessageBlock.setVisible(true);
-      commitMessageBlock.display(patchSetDetail.getInfo().getMessage());
+      commitMessageBlock.display(patchSetDetail.getInfo().getMessage(),
+          commentLinkProcessor);
     } else {
       commitMessageBlock.setVisible(false);
       Util.DETAIL_SVC.patchSetDetail(idSideB,
@@ -405,7 +427,8 @@
             @Override
             public void onSuccess(PatchSetDetail result) {
               commitMessageBlock.setVisible(true);
-              commitMessageBlock.display(result.getInfo().getMessage());
+              commitMessageBlock.display(result.getInfo().getMessage(),
+                  commentLinkProcessor);
             }
           });
     }
@@ -420,14 +443,10 @@
         break;
       }
     }
-    // True if there are differences between the two patch sets
-    boolean hasEdits = !script.getEdits().isEmpty();
-    // True if this change is a mode change or a pure rename/copy
-    boolean hasMeta = !script.getPatchHeader().isEmpty();
 
-    boolean pureMetaChange = !hasEdits && hasMeta;
-
-    if (contentTable instanceof SideBySideTable && pureMetaChange && !contentTable.isDisplayBinary) {
+    if (contentTable instanceof SideBySideTable
+        && contentTable.isPureMetaChange(script)
+        && !contentTable.isDisplayBinary) {
       // User asked for SideBySide (or a link guessed, wrong) and we can't
       // show a pure-rename change there accurately. Switch to
       // the unified view instead. User can set file comments on binary file
@@ -436,6 +455,7 @@
       contentTable.removeFromParent();
       contentTable = new UnifiedDiffTable();
       contentTable.fileList = fileList;
+      contentTable.setCommentLinkProcessor(commentLinkProcessor);
       contentPanel.add(contentTable);
       setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
@@ -472,14 +492,27 @@
     }
 
     intralineFailure = isFirst && script.hasIntralineFailure();
+    intralineTimeout = isFirst && script.hasIntralineTimeout();
   }
 
   @Override
   public void onShowView() {
     super.onShowView();
+    if (prefsHandler == null) {
+      prefsHandler = prefs.addValueChangeHandler(
+          new ValueChangeHandler<AccountDiffPreference>() {
+            @Override
+            public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
+              update(event.getValue());
+            }
+          });
+    }
     if (intralineFailure) {
       intralineFailure = false;
       new ErrorDialog(PatchUtil.C.intralineFailure()).show();
+    } else if (intralineTimeout) {
+      intralineTimeout = false;
+      new ErrorDialog(PatchUtil.C.intralineTimeout()).show();
     }
     if (topView != null && prefs.get().isRetainHeader()) {
       setTopView(topView);
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 a689259..8e76e47 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
@@ -27,10 +27,6 @@
 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.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-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.uibinder.client.UiHandler;
@@ -43,14 +39,13 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtjsonrpc.common.VoidResult;
 
-public class PatchScriptSettingsPanel extends Composite implements
-    HasValueChangeHandlers<AccountDiffPreference> {
+public class PatchScriptSettingsPanel extends Composite {
   private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
 
   interface MyUiBinder extends UiBinder<Widget, PatchScriptSettingsPanel> {
   }
 
-  private ListenableAccountDiffPreference listenablePrefs;
+  private final ListenableAccountDiffPreference listenablePrefs;
   private boolean enableIntralineDifference = true;
   private boolean enableSmallFileFeatures = true;
 
@@ -139,12 +134,6 @@
     display();
   }
 
-  @Override
-  public HandlerRegistration addValueChangeHandler(
-      ValueChangeHandler<AccountDiffPreference> handler) {
-    return super.addHandler(handler, ValueChangeEvent.getType());
-  }
-
   public void setEnabled(final boolean on) {
     if (on) {
       setEnabledCounter++;
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 15ab951..4bdd615 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
@@ -22,10 +22,11 @@
 import com.google.gerrit.client.Gerrit;
 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.common.data.PatchScript.FileMode;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -186,11 +187,16 @@
           lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
         }
       }
-    }else{
+    } else {
       // Display the patch header for binary
       for (final String line : script.getPatchHeader()) {
         appendFileHeader(nc, line);
       }
+      // If there is a safe picture involved, we show it
+      if (script.getDisplayMethodA() == DisplayMethod.IMG
+          || script.getDisplayMethodB() == DisplayMethod.IMG) {
+        appendImageLine(script, nc);
+      }
     }
     if (!hasDifferences(script)) {
       appendNoDifferences(nc);
@@ -210,6 +216,42 @@
     }
   }
 
+  private SafeHtml createImage(String url) {
+    SafeHtmlBuilder m = new SafeHtmlBuilder();
+    m.openElement("img");
+    m.setAttribute("src", url);
+    m.closeElement("img");
+    return m.toSafeHtml();
+  }
+
+  private void appendImageLine(final PatchScript script,
+      final SafeHtmlBuilder m) {
+    m.openTr();
+    m.setAttribute("valign", "center");
+    m.setAttribute("align", "center");
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
+    m.closeTd();
+
+    appendLineNumber(m, false);
+    if (script.getDisplayMethodA() == DisplayMethod.IMG) {
+      final String url = getUrlA();
+      appendLineText(m, DELETE, createImage(url), false, true);
+    } else {
+      appendLineNone(m, DELETE);
+    }
+    if (script.getDisplayMethodB() == DisplayMethod.IMG) {
+      final String url = getUrlB();
+      appendLineText(m, INSERT, createImage(url), false, true);
+    } else {
+      appendLineNone(m, INSERT);
+    }
+
+    appendLineNumber(m, true);
+    m.closeTr();
+  }
+
   private void populateTableHeader(final PatchScript script,
       final PatchSetDetail detail) {
     initHeaders(script, detail);
@@ -400,10 +442,7 @@
     m.addStyleName(Gerrit.RESOURCES.css().iconCell());
     m.closeTd();
 
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.nbsp();
-    m.closeTd();
+    appendLineNumber(m, false);
 
     m.openTd();
     m.setStyleName(Gerrit.RESOURCES.css().sideBySideTableBinaryHeader());
@@ -411,9 +450,7 @@
     m.append(line);
     m.closeTd();
 
-    m.openTd();
-    m.nbsp();
-    m.closeTd();
+    appendLineNumber(m, true);
 
     m.closeTr();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index 82df54a..cacedfd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -23,20 +23,17 @@
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.EditList.Hunk;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
-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.GWT;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
-import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtorm.client.KeyUtil;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -226,94 +223,18 @@
       appendFileHeader(nc, line);
     }
     final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
-    if (!isDisplayBinary) {
-      final SparseHtmlFile a = getSparseHtmlFileA(script);
-      final SparseHtmlFile b = getSparseHtmlFileB(script);
+
+    if (hasDifferences(script)) {
       if (script.getDisplayMethodA() == DisplayMethod.IMG
           || script.getDisplayMethodB() == DisplayMethod.IMG) {
-        final String rawBase = GWT.getHostPageBaseURL() + "cat/";
-
-        nc.openTr();
-        nc.setAttribute("valign", "center");
-        nc.setAttribute("align", "center");
-
-        nc.openTd();
-        nc.nbsp();
-        nc.closeTd();
-
-        nc.openTd();
-        nc.nbsp();
-        nc.closeTd();
-
-        nc.openTd();
-        nc.nbsp();
-        nc.closeTd();
-
-        nc.openTd();
-        if (script.getDisplayMethodA() == DisplayMethod.IMG) {
-          if (idSideA == null) {
-            appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^1");
-          } else {
-            Patch.Key k = new Patch.Key(idSideA, patchKey.get());
-            appendImgTag(nc, rawBase + KeyUtil.encode(k.toString()) + "^0");
-          }
-        }
-        if (script.getDisplayMethodB() == DisplayMethod.IMG) {
-          appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^0");
-        }
-        nc.closeTd();
-
-        nc.closeTr();
+        appendImageDifferences(script, nc);
+      } else if (!isDisplayBinary) {
+        appendTextDifferences(script, nc, lines);
       }
-
-      if (hasDifferences(script)) {
-        final boolean syntaxHighlighting =
-            script.getDiffPrefs().isSyntaxHighlighting();
-        for (final EditList.Hunk hunk : script.getHunks()) {
-          appendHunkHeader(nc, hunk);
-          while (hunk.next()) {
-            if (hunk.isContextLine()) {
-              openLine(nc);
-              appendLineNumberForSideA(nc, hunk.getCurA());
-              appendLineNumberForSideB(nc, hunk.getCurB());
-              appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
-              closeLine(nc);
-              hunk.incBoth();
-              lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
-
-            } else if (hunk.isDeletedA()) {
-              openLine(nc);
-              appendLineNumberForSideA(nc, hunk.getCurA());
-              padLineNumberForSideB(nc);
-              appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
-              closeLine(nc);
-              hunk.incA();
-              lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
-              if (a.size() == hunk.getCurA()
-                  && script.getA().isMissingNewlineAtEnd()) {
-                appendNoLF(nc);
-              }
-
-            } else if (hunk.isInsertedB()) {
-              openLine(nc);
-              padLineNumberForSideA(nc);
-              appendLineNumberForSideB(nc, hunk.getCurB());
-              appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
-              closeLine(nc);
-              hunk.incB();
-              lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
-              if (b.size() == hunk.getCurB()
-                  && script.getB().isMissingNewlineAtEnd()) {
-                appendNoLF(nc);
-              }
-            }
-          }
-        }
-      }
-    }
-    if (!hasDifferences(script)) {
+    } else {
       appendNoDifferences(nc);
     }
+
     resetHtml(nc);
     populateTableHeader(script, detail);
     if (hasDifferences(script)) {
@@ -347,6 +268,94 @@
     }
   }
 
+  private void appendImageLine(final SafeHtmlBuilder nc, final String url,
+      final boolean syntaxHighlighting, final boolean isInsert) {
+    nc.openTr();
+    nc.setAttribute("valign", "center");
+    nc.setAttribute("align", "center");
+
+    nc.openTd();
+    nc.setStyleName(Gerrit.RESOURCES.css().iconCell());
+    nc.closeTd();
+
+    padLineNumberForSideA(nc);
+    padLineNumberForSideB(nc);
+
+    nc.openTd();
+    nc.setStyleName(Gerrit.RESOURCES.css().fileLine());
+    if (isInsert) {
+      setStyleInsert(nc, syntaxHighlighting);
+    } else {
+      setStyleDelete(nc, syntaxHighlighting);
+    }
+    appendImgTag(nc, url);
+    nc.closeTd();
+
+    nc.closeTr();
+  }
+
+  private void appendImageDifferences(final PatchScript script,
+      final SafeHtmlBuilder nc) {
+    final boolean syntaxHighlighting =
+        script.getDiffPrefs().isSyntaxHighlighting();
+    if (script.getDisplayMethodA() == DisplayMethod.IMG) {
+      final String url = getUrlA();
+      appendImageLine(nc, url, syntaxHighlighting, false);
+    }
+    if (script.getDisplayMethodB() == DisplayMethod.IMG) {
+      final String url = getUrlB();
+      appendImageLine(nc, url, syntaxHighlighting, true);
+    }
+  }
+
+  private void appendTextDifferences(final PatchScript script,
+      final SafeHtmlBuilder nc, final ArrayList<PatchLine> lines) {
+    final SparseHtmlFile a = getSparseHtmlFileA(script);
+    final SparseHtmlFile b = getSparseHtmlFileB(script);
+    final boolean syntaxHighlighting =
+        script.getDiffPrefs().isSyntaxHighlighting();
+    for (final EditList.Hunk hunk : script.getHunks()) {
+      appendHunkHeader(nc, hunk);
+      while (hunk.next()) {
+        if (hunk.isContextLine()) {
+          openLine(nc);
+          appendLineNumberForSideA(nc, hunk.getCurA());
+          appendLineNumberForSideB(nc, hunk.getCurB());
+          appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
+          closeLine(nc);
+          hunk.incBoth();
+          lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
+
+        } else if (hunk.isDeletedA()) {
+          openLine(nc);
+          appendLineNumberForSideA(nc, hunk.getCurA());
+          padLineNumberForSideB(nc);
+          appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
+          closeLine(nc);
+          hunk.incA();
+          lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
+          if (a.size() == hunk.getCurA()
+              && script.getA().isMissingNewlineAtEnd()) {
+            appendNoLF(nc);
+          }
+
+        } else if (hunk.isInsertedB()) {
+          openLine(nc);
+          padLineNumberForSideA(nc);
+          appendLineNumberForSideB(nc, hunk.getCurB());
+          appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
+          closeLine(nc);
+          hunk.incB();
+          lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+          if (b.size() == hunk.getCurB()
+              && script.getB().isMissingNewlineAtEnd()) {
+            appendNoLF(nc);
+          }
+        }
+      }
+    }
+  }
+
   @Override
   public void display(final CommentDetail cd, boolean expandComments) {
     if (cd.isEmpty()) {
@@ -519,6 +528,22 @@
     }
   }
 
+  private void setStyleDelete(final SafeHtmlBuilder m,
+      boolean syntaxHighlighting) {
+    m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE());
+    if (syntaxHighlighting) {
+      m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
+    }
+  }
+
+  private void setStyleInsert(final SafeHtmlBuilder m,
+      boolean syntaxHighlighting) {
+    m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT());
+    if (syntaxHighlighting) {
+      m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
+    }
+  }
+
   private void appendLineText(final SafeHtmlBuilder m,
       boolean syntaxHighlighting, final PatchLine.Type type,
       final SparseHtmlFile src, final int i) {
@@ -533,18 +558,12 @@
         m.append(text);
         break;
       case DELETE:
-        m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE());
-        if (syntaxHighlighting) {
-          m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
-        }
+        setStyleDelete(m, syntaxHighlighting);
         m.append("-");
         m.append(text);
         break;
       case INSERT:
-        m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT());
-        if (syntaxHighlighting) {
-          m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
-        }
+        setStyleInsert(m, syntaxHighlighting);
         m.append("+");
         m.append(text);
         break;
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..6633fca
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.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.projects;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class BranchInfo extends JavaScriptObject {
+  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
new file mode 100644
index 0000000..522d348
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.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.projects;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwtexpui.safehtml.client.FindReplace;
+import com.google.gwtexpui.safehtml.client.LinkFindReplace;
+import com.google.gwtexpui.safehtml.client.RawFindReplace;
+
+import java.util.ArrayList;
+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()
+  /*-{ 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()
+  /*-{ 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()
+  /*-{ 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()
+  /*-{ return this.use_signed_off_by; }-*/;
+
+  private final native NativeMap<CommentLinkInfo> commentlinks0()
+  /*-{ return this.commentlinks; }-*/;
+  final List<FindReplace> commentlinks() {
+    JsArray<CommentLinkInfo> cls = commentlinks0().values();
+    List<FindReplace> commentLinks = new ArrayList<FindReplace>(cls.length());
+    for (int i = 0; i < cls.length(); i++) {
+      CommentLinkInfo cl = cls.get(i);
+      if (!cl.enabled()) {
+        continue;
+      }
+      if (cl.link() != null) {
+        commentLinks.add(new LinkFindReplace(cl.match(), cl.link()));
+      } else {
+        commentLinks.add(new RawFindReplace(cl.match(), cl.html()));
+      }
+    }
+    return commentLinks;
+  }
+
+  final native ThemeInfo theme() /*-{ return this.theme; }-*/;
+
+  protected ConfigInfo() {
+  }
+
+  static class CommentLinkInfo extends JavaScriptObject {
+    final native String match() /*-{ return this.match; }-*/;
+    final native String link() /*-{ return this.link; }-*/;
+    final native String html() /*-{ return this.html; }-*/;
+    final native boolean enabled() /*-{
+      return !this.hasOwnProperty('enabled') || this.enabled;
+    }-*/;
+
+    protected CommentLinkInfo() {
+    }
+  }
+}
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
new file mode 100644
index 0000000..16406f4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.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.client.projects;
+
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Cache of {@link ConfigInfo} objects by project name. */
+public class ConfigInfoCache {
+  private static final int LIMIT = 25;
+  private static final ConfigInfoCache instance =
+      GWT.create(ConfigInfoCache.class);
+
+  public static class Entry {
+    private final ConfigInfo info;
+    private CommentLinkProcessor commentLinkProcessor;
+
+    private Entry(ConfigInfo info) {
+      this.info = info;
+    }
+
+    public CommentLinkProcessor getCommentLinkProcessor() {
+      if (commentLinkProcessor == null) {
+        commentLinkProcessor = new CommentLinkProcessor(info.commentlinks());
+      }
+      return commentLinkProcessor;
+    }
+
+    public ThemeInfo getTheme() {
+      return info.theme();
+    }
+  }
+
+  public static void get(Project.NameKey name, AsyncCallback<Entry> cb) {
+    instance.getImpl(name, cb);
+  }
+
+  private final LinkedHashMap<String, Entry> cache;
+
+  protected ConfigInfoCache() {
+    cache = new LinkedHashMap<String, Entry>(LIMIT) {
+      private static final long serialVersionUID = 1L;
+
+      @Override
+      protected boolean removeEldestEntry(
+          Map.Entry<String, ConfigInfoCache.Entry> e) {
+        return size() > LIMIT;
+      }
+    };
+  }
+
+  private void getImpl(final Project.NameKey name,
+      final AsyncCallback<Entry> cb) {
+    Entry e = cache.get(name.get());
+    if (e != null) {
+      cb.onSuccess(e);
+      return;
+    }
+    ProjectApi.config(name).get(new AsyncCallback<ConfigInfo>() {
+      @Override
+      public void onSuccess(ConfigInfo result) {
+        Entry e = new Entry(result);
+        cache.put(name.get(), e);
+        cb.onSuccess(e);
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        cb.onFailure(caught);
+      }
+    });
+  }
+}
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 a6676dc..63bcd64 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
@@ -15,6 +15,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
@@ -32,6 +33,19 @@
         .put(input, asyncCallback);
   }
 
+  /** Create a new branch */
+  public static void createBranch(Project.NameKey projectName, String ref,
+      String revision, AsyncCallback<BranchInfo> asyncCallback) {
+    BranchInput input = BranchInput.create();
+    input.setRevision(revision);
+    new RestApi("/projects/").id(projectName.get()).view("branches").id(ref)
+        .ifNoneMatch().put(input, asyncCallback);
+  }
+
+  static RestApi config(Project.NameKey name) {
+    return new RestApi("/projects/").id(name.get()).view("config");
+  }
+
   private static class ProjectInput extends JavaScriptObject {
     static ProjectInput create() {
       return (ProjectInput) createObject();
@@ -48,4 +62,15 @@
 
     final native void setCreateEmptyCommit(boolean cc) /*-{ if(cc)this.create_empty_commit=cc; }-*/;
   }
+
+  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; }-*/;
+  }
 }
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
new file mode 100644
index 0000000..67b6077
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.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.git;
+
+package com.google.gerrit.client.projects;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ThemeInfo extends JavaScriptObject {
+  public final native String css() /*-{ return this.css; }-*/;
+  public final native String header() /*-{ return this.header; }-*/;
+  public final native String footer() /*-{ return this.footer; }-*/;
+
+  protected ThemeInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
similarity index 65%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index fd52777..ef52176 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.ui;
 
+import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
@@ -22,33 +23,46 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gwt.user.client.ui.FlowPanel;
 
 /** Link to any user's account dashboard. */
-public class AccountLink extends InlineHyperlink {
+public class AccountLinkPanel extends FlowPanel {
   /** Create a link after locating account details from an active cache. */
-  public static AccountLink link(AccountInfoCache cache, Account.Id id) {
+  public static AccountLinkPanel link(AccountInfoCache cache, Account.Id id) {
     com.google.gerrit.common.data.AccountInfo ai = cache.get(id);
-    return ai != null ? new AccountLink(ai) : null;
+    return ai != null ? new AccountLinkPanel(ai) : null;
   }
 
-  public AccountLink(com.google.gerrit.common.data.AccountInfo ai) {
+  public AccountLinkPanel(com.google.gerrit.common.data.AccountInfo ai) {
     this(FormatUtil.asInfo(ai));
   }
 
-  public AccountLink(UserIdentity ident) {
+  public AccountLinkPanel(UserIdentity ident) {
     this(AccountInfo.create(
         ident.getAccount().get(),
         ident.getName(),
         ident.getEmail()));
   }
 
-  public AccountLink(AccountInfo info) {
+  public AccountLinkPanel(AccountInfo info) {
     this(info, Change.Status.NEW);
   }
 
-  public AccountLink(AccountInfo info, Change.Status status) {
-    super(FormatUtil.name(info), PageLinks.toAccountQuery(owner(info), status));
-    setTitle(FormatUtil.nameEmail(info));
+  public AccountLinkPanel(AccountInfo info, Change.Status status) {
+    addStyleName(Gerrit.RESOURCES.css().accountLinkPanel());
+
+    InlineHyperlink l =
+        new InlineHyperlink(FormatUtil.name(info), PageLinks.toAccountQuery(
+            owner(info), status)) {
+      @Override
+      public void go() {
+        Gerrit.display(getTargetHistoryToken());
+      }
+    };
+    l.setTitle(FormatUtil.nameEmail(info));
+
+    add(new AvatarImage(info, 16));
+    add(l);
   }
 
   private static String owner(AccountInfo ai) {
@@ -62,9 +76,4 @@
       return "";
     }
   }
-
-  @Override
-  public void go() {
-    Gerrit.display(getTargetHistoryToken());
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index b113a3b..0bf0ea9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -53,11 +53,11 @@
     }
 
     public String getDisplayString() {
-      return FormatUtil.nameEmail(info);
+      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
 
     public String getReplacementString() {
-      return FormatUtil.nameEmail(info);
+      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
   }
 }
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/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
new file mode 100644
index 0000000..4a449cc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.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.client.ui;
+
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.data.ListBranchesResult;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+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<Branch> branches;
+
+  public CherryPickDialog(final FocusWidget enableOnFailure, Project.NameKey project) {
+    super(enableOnFailure, true, Util.C.cherryPickTitle(), Util.C
+        .cherryPickCommitMessage());
+    com.google.gerrit.client.account.Util.PROJECT_SVC.listBranches(project,
+        new GerritCallback<ListBranchesResult>() {
+          @Override
+          public void onSuccess(ListBranchesResult result) {
+            branches = result.getBranches();
+          }
+        });
+
+    newBranch = new SuggestBox(new HighlightSuggestOracle() {
+      @Override
+      protected void onRequestSuggestions(Request request, Callback done) {
+        LinkedList<BranchSuggestion> suggestions =
+            new LinkedList<BranchSuggestion>();
+        for (final Branch b : branches) {
+          if (b.getName().indexOf(request.getQuery()) >= 0) {
+            suggestions.add(new BranchSuggestion(b));
+          }
+        }
+        done.onSuggestionsReady(request, new Response(suggestions));
+      }
+    });
+
+    newBranch.setWidth("70ex");
+    message.setCharacterWidth(70);
+    panel.insert(newBranch, 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 Branch branch;
+
+    public BranchSuggestion(Branch branch) {
+      this.branch = branch;
+    }
+
+    @Override
+    public String getDisplayString() {
+      return branch.getName();
+    }
+
+    @Override
+    public String getReplacementString() {
+      return branch.getShortName();
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
index a3c7a3c..10cd1f0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
+import com.google.gwtexpui.safehtml.client.FindReplace;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.ArrayList;
@@ -25,17 +25,23 @@
 import java.util.List;
 
 public class CommentLinkProcessor {
-  public static SafeHtml apply(SafeHtml buf) {
-    try {
-      return buf.replaceAll(Gerrit.getConfig().getCommentLinks());
+  private List<FindReplace> commentLinks;
 
+  public CommentLinkProcessor(List<FindReplace> commentLinks) {
+    this.commentLinks = commentLinks;
+  }
+
+  public SafeHtml apply(SafeHtml buf) {
+    try {
+      return buf.replaceAll(commentLinks);
     } catch (RuntimeException err) {
       // One or more of the patterns isn't valid on this browser.
       // Try to filter the list down and remove the invalid ones.
 
-      List<RegexFindReplace> safe = new ArrayList<RegexFindReplace>();
+      List<FindReplace> safe = new ArrayList<FindReplace>(commentLinks.size());
+
       List<PatternError> bad = new ArrayList<PatternError>();
-      for (RegexFindReplace r : Gerrit.getConfig().getCommentLinks()) {
+      for (FindReplace r : commentLinks) {
         try {
           buf.replaceAll(Collections.singletonList(r));
           safe.add(r);
@@ -50,7 +56,7 @@
         for (PatternError e : bad) {
           msg.append("\n");
           msg.append("\"");
-          msg.append(e.pattern.find());
+          msg.append(e.pattern.pattern().getSource());
           msg.append("\": ");
           msg.append(e.errorMessage);
         }
@@ -67,28 +73,25 @@
       }
 
       try {
-        Gerrit.getConfig().setCommentLinks(safe);
+        commentLinks = safe;
         return buf.replaceAll(safe);
       } catch (RuntimeException err2) {
         // To heck with it. The patterns passed individually above but
-        // failed as a group? Just drop them all and render without.
+        // failed as a group? Just render without.
         //
-        Gerrit.getConfig().setCommentLinks(null);
+        commentLinks = null;
         return buf;
       }
     }
   }
 
   private static class PatternError {
-    RegexFindReplace pattern;
+    FindReplace pattern;
     String errorMessage;
 
-    PatternError(RegexFindReplace r, String w) {
+    PatternError(FindReplace r, String w) {
       pattern = r;
       errorMessage = w;
     }
   }
-
-  private CommentLinkProcessor() {
-  }
 }
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 8f8c7ea..05c6b5a 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.ui;
 
+import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
@@ -53,22 +54,25 @@
   private final InlineLabel messageSummary;
   private final FlowPanel content;
   private final DoubleClickHTML messageText;
+  private CommentLinkProcessor commentLinkProcessor;
   private FlowPanel buttons;
   private boolean recent;
 
-  public CommentPanel(final AccountInfo author, final Date when, String message) {
-    this();
+  public CommentPanel(final AccountInfo author, final Date when, String message,
+      CommentLinkProcessor commentLinkProcessor) {
+    this(commentLinkProcessor);
 
     setMessageText(message);
-    setAuthorNameText(FormatUtil.name(author));
+    setAuthorNameText(author, FormatUtil.name(author));
     setDateText(FormatUtil.shortFormatDayTime(when));
 
     final CellFormatter fmt = header.getCellFormatter();
-    fmt.getElement(0, 0).setTitle(FormatUtil.nameEmail(author));
-    fmt.getElement(0, 2).setTitle(FormatUtil.mediumFormat(when));
+    fmt.getElement(0, 1).setTitle(FormatUtil.nameEmail(author));
+    fmt.getElement(0, 3).setTitle(FormatUtil.mediumFormat(when));
   }
 
-  protected CommentPanel() {
+  protected CommentPanel(CommentLinkProcessor commentLinkProcessor) {
+    this.commentLinkProcessor = commentLinkProcessor;
     final FlowPanel body = new FlowPanel();
     initWidget(body);
     setStyleName(Gerrit.RESOURCES.css().commentPanel());
@@ -84,14 +88,14 @@
         setOpen(!isOpen());
       }
     });
-    header.setText(0, 0, "");
-    header.setWidget(0, 1, messageSummary);
-    header.setText(0, 2, "");
+    header.setText(0, 1, "");
+    header.setWidget(0, 2, messageSummary);
+    header.setText(0, 3, "");
     final CellFormatter fmt = header.getCellFormatter();
-    fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().commentPanelAuthorCell());
-    fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().commentPanelSummaryCell());
-    fmt.setStyleName(0, 2, Gerrit.RESOURCES.css().commentPanelDateCell());
-    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+    fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().commentPanelAuthorCell());
+    fmt.setStyleName(0, 2, Gerrit.RESOURCES.css().commentPanelSummaryCell());
+    fmt.setStyleName(0, 3, Gerrit.RESOURCES.css().commentPanelDateCell());
+    fmt.setHorizontalAlignment(0, 3, HasHorizontalAlignment.ALIGN_RIGHT);
     body.add(header);
 
     content = new FlowPanel();
@@ -118,16 +122,17 @@
 
     messageSummary.setText(summarize(message));
     SafeHtml buf = new SafeHtmlBuilder().append(message).wikify();
-    buf = CommentLinkProcessor.apply(buf);
+    buf = commentLinkProcessor.apply(buf);
     SafeHtml.set(messageText, buf);
   }
 
-  public void setAuthorNameText(final String nameText) {
-    header.setText(0, 0, nameText);
+  public void setAuthorNameText(final AccountInfo author, final String nameText) {
+    header.setWidget(0, 0, new AvatarImage(author, 26));
+    header.setText(0, 1, nameText);
   }
 
   protected void setDateText(final String dateText) {
-    header.setText(0, 2, dateText);
+    header.setText(0, 3, dateText);
   }
 
   protected void setMessageTextVisible(final boolean show) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
index 5c3e127..47bca2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
@@ -23,8 +23,11 @@
   }
 
   public void set(final T value) {
-    oldValue = get();
-    super.set(value);
-    oldValue = null; // allow it to be gced before the next event
+    try {
+      oldValue = get();
+      super.set(value);
+    } finally {
+      oldValue = null; // allow it to be gced before the next event
+    }
   }
 }
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 b574db1..a7d49a4 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
@@ -33,6 +33,7 @@
 import com.google.gwt.user.client.ui.FocusWidget;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.ValueBoxBase;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -139,6 +140,17 @@
     if (widget.isEnabled() ||
         ! (e.getSource() instanceof FocusWidget) ||
         ! ((FocusWidget) e.getSource()).isEnabled() ) {
+      if (e.getSource() instanceof ValueBoxBase) {
+        final TextBoxBase box = ((TextBoxBase) e.getSource());
+        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+          @Override
+          public void execute() {
+            if (box.getValue().trim().length() == 0) {
+              widget.setEnabled(false);
+            }
+          }
+        });
+      }
       return;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
index f3b1439..3f1de2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
@@ -65,7 +65,7 @@
     public String getDisplayString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
-        return FormatUtil.nameEmail(accountInfo);
+        return FormatUtil.nameEmail(FormatUtil.asInfo(accountInfo));
       }
       return reviewerInfo.getGroup().getName() + " ("
           + Util.C.suggestedGroupLabel() + ")";
@@ -74,7 +74,7 @@
     public String getReplacementString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
-        return FormatUtil.nameEmail(accountInfo);
+        return FormatUtil.nameEmail(FormatUtil.asInfo(accountInfo));
       }
       return reviewerInfo.getGroup().getName();
     }
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 e7c2d84..a26db05 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ThemeInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -41,6 +42,9 @@
   private String windowTitle;
   private Widget titleWidget;
 
+  private ThemeInfo theme;
+  private boolean setTheme;
+
   protected Screen() {
     initWidget(new FlowPanel());
     setStyleName(Gerrit.RESOURCES.css().screen());
@@ -54,6 +58,14 @@
     }
   }
 
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    if (setTheme) {
+      Gerrit.THEMER.clear();
+    }
+  }
+
   public void registerKeys() {
   }
 
@@ -124,6 +136,10 @@
     body.add(w);
   }
 
+  protected void setTheme(final ThemeInfo t) {
+    theme = t;
+  }
+
   /** Get the history token for this screen. */
   public String getToken() {
     return token;
@@ -167,5 +183,12 @@
     Gerrit.EVENT_BUS.fireEvent(new ScreenLoadEvent(this));
     Gerrit.setQueryString(null);
     registerKeys();
+
+    if (theme != null) {
+      Gerrit.THEMER.set(theme);
+      setTheme = true;
+    } else {
+      Gerrit.THEMER.clear();
+    }
   }
 }
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
new file mode 100644
index 0000000..51f951d
--- /dev/null
+++ b/gerrit-httpd/BUCK
@@ -0,0 +1,58 @@
+java_library2(
+  name = 'httpd',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/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:jsr305',
+    '//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_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
index 87f2f10..ed4a63d 100644
--- a/gerrit-httpd/pom.xml
+++ b/gerrit-httpd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
@@ -66,12 +66,6 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-launcher</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-server</artifactId>
       <version>${project.version}</version>
     </dependency>
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 aa56ae9..7241624 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
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -34,12 +33,8 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.net.MalformedURLException;
-import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
 
 import javax.servlet.ServletContext;
 
@@ -148,31 +143,6 @@
       config.setSshdAddress(sshInfo.getHostKeys().get(0).getHost());
     }
 
-    List<RegexFindReplace> links = new ArrayList<RegexFindReplace>();
-    for (String name : cfg.getSubsections("commentlink")) {
-      String match = cfg.getString("commentlink", name, "match");
-
-      // Unfortunately this validation isn't entirely complete. Clients
-      // can have exceptions trying to evaluate the pattern if they don't
-      // support a token used, even if the server does support the token.
-      //
-      // At the minimum, we can trap problems related to unmatched groups.
-      try {
-        Pattern.compile(match);
-      } catch (PatternSyntaxException e) {
-        throw new ProvisionException("Invalid pattern \"" + match
-            + "\" in commentlink." + name + ".match: " + e.getMessage());
-      }
-
-      String link = cfg.getString("commentlink", name, "link");
-      String html = cfg.getString("commentlink", name, "html");
-      if (html == null || html.isEmpty()) {
-        html = "<a href=\"" + link + "\">$&</a>";
-      }
-      links.add(new RegexFindReplace(match, html));
-    }
-    config.setCommentLinks(links);
-
     return config;
   }
 
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 7de4bc3..22d7568 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
@@ -55,6 +55,7 @@
     type.setProject(cfg.getString("gitweb", null, "project"));
     type.setRevision(cfg.getString("gitweb", null, "revision"));
     type.setFileHistory(cfg.getString("gitweb", null, "filehistory"));
+    type.setLinkDrafts(cfg.getBoolean("gitweb", null, "linkdrafts", 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/HtmlDomUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
index 0a93f80..c47552d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
@@ -53,7 +53,7 @@
 
   /** DOCTYPE for a standards mode HTML document. */
   public static final String HTML_STRICT =
-      "-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd";
+      "-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd";
 
   /** Convert a document to a UTF-8 byte sequence. */
   public static byte[] toUTF8(final Document hostDoc) throws IOException {
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..d87e4af 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
@@ -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..f1bddeb 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
@@ -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/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index b93df43..bd25faf 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
@@ -81,7 +81,6 @@
     serve("/signout").with(HttpLogoutServlet.class);
     serve("/ssh_info").with(SshInfoServlet.class);
     serve("/static/*").with(StaticServlet.class);
-    serve("/tools/*").with(ToolServlet.class);
 
     serve("/Main.class").with(notFound());
     serve("/com/google/gerrit/launcher/*").with(notFound());
@@ -100,6 +99,7 @@
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
 
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
+    serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
     serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
     serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
     serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 83f4088..28e361c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -60,14 +61,18 @@
   private final SchemaFactory<ReviewDb> schema;
   private final Provider<WebSession> webSession;
   private final AccountManager accountManager;
+  private final SiteHeaderFooter headers;
 
   @Inject
   BecomeAnyAccountLoginServlet(final Provider<WebSession> ws,
       final SchemaFactory<ReviewDb> sf,
-      final AccountManager am, final ServletContext servletContext) {
+      final AccountManager am,
+      final ServletContext servletContext,
+      SiteHeaderFooter shf) {
     webSession = ws;
     schema = sf;
     accountManager = am;
+    headers = shf;
   }
 
   @Override
@@ -149,7 +154,7 @@
 
   private byte[] prepareHtmlOutput() throws IOException, OrmException {
     final String pageName = "BecomeAnyAccount.html";
-    final Document doc = HtmlDomUtil.parseFile(getClass(), pageName);
+    Document doc = headers.parse(getClass(), pageName);
     if (doc == null) {
       throw new FileNotFoundException("No " + pageName + " in webapp");
     }
@@ -168,7 +173,7 @@
         String displayName;
         if (a.getUserName() != null) {
           displayName = a.getUserName();
-        } else if (a.getFullName() != null) {
+        } else if (a.getFullName() != null && !a.getFullName().isEmpty()) {
           displayName = a.getFullName();
         } else if (a.getPreferredEmail() != null) {
           displayName = a.getPreferredEmail();
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 f6cd8c1..adca95e 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
@@ -119,14 +119,17 @@
     WebSession session = sessionProvider.get();
     if (session.isSignedIn()) {
       String user = getRemoteUser(req);
-      AccountExternalId.Key id = session.getLastLoginExternalId();
-      return user != null
-          && id != null
-          && id.equals(new AccountExternalId.Key(SCHEME_GERRIT, user));
+      return user == null || correctUser(user, session);
     }
     return false;
   }
 
+  private static boolean correctUser(String user, WebSession session) {
+    AccountExternalId.Key id = session.getLastLoginExternalId();
+    return id != null
+        && id.equals(new AccountExternalId.Key(SCHEME_GERRIT, user));
+  }
+
   String getRemoteUser(HttpServletRequest req) {
     if (AUTHORIZATION.equals(loginHeader)) {
       String user = emptyToNull(req.getRemoteUser());
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 71db67e..cfae86c 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountUserNameException;
@@ -55,14 +56,17 @@
   private final AccountManager accountManager;
   private final Provider<WebSession> webSession;
   private final Provider<String> urlProvider;
+  private final SiteHeaderFooter headers;
 
   @Inject
   LdapLoginServlet(AccountManager accountManager,
       Provider<WebSession> webSession,
-      @CanonicalWebUrl @Nullable Provider<String> urlProvider) {
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      SiteHeaderFooter headers) {
     this.accountManager = accountManager;
     this.webSession = webSession;
     this.urlProvider = urlProvider;
+    this.headers = headers;
 
     if (Strings.isNullOrEmpty(urlProvider.get())) {
       log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
@@ -78,8 +82,7 @@
       cancel += "#" + token;
     }
 
-    Document doc =
-        HtmlDomUtil.parseFile(LdapLoginServlet.class, "LoginForm.html");
+    Document doc = headers.parse(LdapLoginServlet.class, "LoginForm.html");
     HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
     HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
     HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
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 ba7a5b8..3281190 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
@@ -32,7 +32,6 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.GitWebConfig;
-import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -140,7 +139,10 @@
 
   private void makeSiteConfig(final SitePaths site,
       final GerritConfig gerritConfig) throws IOException {
-    final File myconf = GerritLauncher.createTempFile("gitweb_config", ".perl");
+    if (!site.tmp_dir.exists()) {
+      site.tmp_dir.mkdirs();
+    }
+    File myconf = File.createTempFile("gitweb_config", ".perl", site.tmp_dir);
 
     // To make our configuration file only readable or writable by us;
     // this reduces the chances of someone tampering with the file.
@@ -617,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 549c239..463f2d6 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
@@ -63,6 +63,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;
@@ -172,7 +173,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);
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/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/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 6cdd9bd..b1fd412 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;
@@ -275,6 +276,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();
@@ -352,11 +354,9 @@
 
       String css = HtmlDomUtil.readFile(src.getParentFile(), src.getName());
       if (css == null) {
-        banner.getParentNode().removeChild(banner);
         return info;
       }
 
-      banner.removeAttribute("id");
       banner.appendChild(hostDoc.createCDATASection("\n" + css + "\n"));
       return info;
     }
@@ -375,7 +375,6 @@
 
       Document html = HtmlDomUtil.parseFile(src);
       if (html == null) {
-        banner.getParentNode().removeChild(banner);
         return info;
       }
 
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..06f960e 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
@@ -159,7 +159,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 a2aa191..92a5e41 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,7 @@
 
 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;
@@ -26,6 +27,7 @@
 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,6 +40,8 @@
 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;
@@ -61,13 +65,13 @@
 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;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 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.gson.ExclusionStrategy;
@@ -211,7 +215,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()
@@ -302,11 +308,7 @@
       }
       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);
@@ -587,9 +589,7 @@
       Multimap<String, String> config) {
     final Set<String> want = Sets.newHashSet();
     for (String p : config.get("fields")) {
-      Iterables.addAll(want, Splitter.on(',')
-          .omitEmptyStrings().trimResults()
-          .split(p));
+      Iterables.addAll(want, OptionUtil.splitOptionValue(p));
     }
     if (!want.isEmpty()) {
       gb.addSerializationExclusionStrategy(new ExclusionStrategy() {
@@ -628,10 +628,28 @@
       HttpServletResponse res,
       BinaryResult bin) throws IOException {
     try {
-      res.setContentType(bin.getContentType());
       OutputStream dst = res.getOutputStream();
       try {
         long len = bin.getContentLength();
+        if (bin.isBase64() && 0 <= len && len <= (10 << 20)) {
+          final TemporaryBuffer.Heap buf = base64(bin);
+          len = buf.length();
+          base64(res, bin);
+          bin = new BinaryResult() {
+            @Override
+            public void writeTo(OutputStream os) throws IOException {
+              buf.writeTo(os, null);
+            }
+          }.setContentLength(len);
+        } else if (bin.isBase64()) {
+          len = -1;
+          base64(res, bin);
+          dst = BaseEncoding.base64().encodingStream(
+              new OutputStreamWriter(dst, Charsets.ISO_8859_1));
+        } else {
+          res.setContentType(bin.getContentType());
+        }
+
         boolean gzip = bin.canGzip() && acceptsGzip(req);
         if (gzip && 256 <= len && len <= (10 << 20)) {
           TemporaryBuffer.Heap buf = compress(bin);
@@ -657,6 +675,12 @@
     }
   }
 
+  private static void base64(HttpServletResponse res, BinaryResult bin) {
+    res.setContentType("text/plain; charset=ISO-8859-1");
+    res.setHeader("X-FYI-Content-Encoding", "base64");
+    res.setHeader("X-FYI-Content-Type", bin.getContentType());
+  }
+
   private static void replyUncompressed(HttpServletResponse res,
       OutputStream dst, BinaryResult bin, long len) throws IOException {
     if (0 <= len && len < Integer.MAX_VALUE) {
@@ -843,6 +867,18 @@
     return false;
   }
 
+  private static TemporaryBuffer.Heap base64(BinaryResult bin)
+      throws IOException {
+    int len = (int) bin.getContentLength();
+    int max = 4 * IntMath.divide(len, 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 buf;
+  }
+
   private static TemporaryBuffer.Heap compress(BinaryResult bin)
       throws IOException {
     TemporaryBuffer.Heap buf = heap(20 << 20);
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 b124824..3277992 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
@@ -72,7 +72,11 @@
     } catch (InvalidQueryException e) {
       callback.onFailure(e);
     } catch (NoSuchProjectException e) {
-      callback.onFailure(new NoSuchEntityException());
+      if (e.getMessage() != null) {
+        callback.onFailure(new NoSuchEntityException(e.getMessage()));
+      } else {
+        callback.onFailure(new NoSuchEntityException());
+      }
     } catch (NoSuchGroupException e) {
       callback.onFailure(new NoSuchEntityException());
     } catch (OrmRuntimeException e) {
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 559c270..7690f5c 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
@@ -46,6 +46,7 @@
 
 import org.eclipse.jgit.lib.Config;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -201,8 +202,9 @@
 
   private List<GroupReference> suggestAccountGroup(
       @Nullable final ProjectControl projectControl, final String query, final int limit) {
-    final int n = limit <= 0 ? 10 : Math.min(limit, 10);
-    return Lists.newArrayList(Iterables.limit(groupBackend.suggest(query), n));
+    return Lists.newArrayList(Iterables.limit(
+        groupBackend.suggest(query, projectControl),
+        limit <= 0 ? 10 : Math.min(limit, 10)));
   }
 
   @Override
@@ -217,7 +219,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);
@@ -272,7 +275,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;
     }
@@ -295,6 +298,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/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 120b9af..4ad0725 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
+import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -136,6 +137,8 @@
 
     detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
 
+    detail.setCanCherryPick(control.getProjectControl().canPushToAtLeastOneRef() == Capable.OK);
+
     detail.setCanEdit(control.getRefControl().canWrite());
     detail.setCanEditCommitMessage(change.getStatus().isOpen() && control.canAddPatchSet());
     detail.setCanEditTopicName(control.canEditTopicName());
@@ -177,13 +180,25 @@
   private void loadPatchSets() throws OrmException {
     ResultSet<PatchSet> source = db.patchSets().byChange(changeId);
     List<PatchSet> patches = new ArrayList<PatchSet>();
+    Set<PatchSet.Id> patchesWithDraftComments = new HashSet<PatchSet.Id>();
+    final CurrentUser user = control.getCurrentUser();
+    final Account.Id me =
+        user instanceof IdentifiedUser ? ((IdentifiedUser) user).getAccountId()
+            : null;
     for (PatchSet ps : source) {
+      final PatchSet.Id psId = ps.getId();
       if (control.isPatchVisible(ps, db)) {
         patches.add(ps);
+        if (me != null
+            && db.patchComments().draftByPatchSetAuthor(psId, me)
+                .iterator().hasNext()) {
+          patchesWithDraftComments.add(psId);
+        }
       }
-      patchsetsById.put(ps.getId(), ps);
+      patchsetsById.put(psId, ps);
     }
     detail.setPatchSets(patches);
+    detail.setPatchSetsWithDraftComments(patchesWithDraftComments);
   }
 
   private void loadMessages() throws OrmException {
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..4bb182f 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,6 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
@@ -25,12 +24,11 @@
 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.change.PatchSetInserter;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 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;
@@ -61,21 +59,14 @@
   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 patchSetInserter;
 
   @Inject
   EditCommitMessageHandler(final ChangeControl.Factory changeControlFactory,
@@ -83,29 +74,24 @@
       final ChangeDetailFactory.Factory changeDetailFactory,
       final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
       @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message, final ChangeHooks hooks,
+      @Assisted @Nullable final String message,
       final CommitValidators.Factory commitValidatorsFactory,
       final GitRepositoryManager gitManager,
-      final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated gitRefUpdated,
       @GerritPersonIdent final PersonIdent myIdent,
-      TrackingFooters trackingFooters) {
+      final PatchSetInserter patchSetInserter) {
     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.patchSetInserter = patchSetInserter;
   }
 
   @Override
@@ -132,8 +118,7 @@
           commitValidatorsFactory.create(control.getRefControl(), new NoSshInfo(), git);
 
       ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
-          commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, gitRefUpdated, myIdent,
-          trackingFooters);
+          commitMessageEditedSenderFactory, git, gitRefUpdated, myIdent, patchSetInserter);
 
       return changeDetailFactory.create(changeId).call();
     } finally {
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 95a8e26..868b625 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
@@ -16,18 +16,23 @@
 
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.webui.UiCommand;
 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.UiCommands;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListKey;
@@ -44,6 +49,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -67,6 +73,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 +90,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 +98,7 @@
     this.db = db;
     this.patchListCache = patchListCache;
     this.changeControlFactory = changeControlFactory;
+    this.revisions = revisions;
 
     this.psIdBase = psIdBase;
     this.psIdNew = psIdNew;
@@ -106,7 +115,7 @@
         throw new NoSuchEntityException();
       }
     }
-
+    projectKey = control.getProject().getNameKey();
     final PatchList list;
 
     try {
@@ -114,8 +123,6 @@
         oldId = toObjectId(psIdBase);
         newId = toObjectId(psIdNew);
 
-        projectKey = control.getProject().getNameKey();
-
         list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
       } else { // OK, means use base to compare
         list = patchListCache.get(control.getChange(), patchSet);
@@ -139,6 +146,7 @@
 
     detail = new PatchSetDetail();
     detail.setPatchSet(patchSet);
+    detail.setProject(projectKey);
 
     detail.setInfo(infoFactory.get(db, psIdNew));
     detail.setPatches(patches);
@@ -165,6 +173,10 @@
       }
     }
 
+    detail.setCommands(UiCommands.sorted(UiCommands.from(
+      revisions,
+      new RevisionResource(new ChangeResource(control), patchSet),
+      EnumSet.of(UiCommand.Place.PATCHSET_ACTION_PANEL))));
     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/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
index 816bbef..5019403 100644
--- 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
@@ -133,6 +133,7 @@
       throws IOException {
     boolean intralineDifferenceIsPossible = true;
     boolean intralineFailure = false;
+    boolean intralineTimeout = false;
 
     a.path = oldName(content);
     b.path = newName(content);
@@ -160,10 +161,14 @@
             break;
 
           case ERROR:
-          case TIMEOUT:
             intralineDifferenceIsPossible = false;
             intralineFailure = true;
             break;
+
+          case TIMEOUT:
+            intralineDifferenceIsPossible = false;
+            intralineTimeout = true;
+            break;
         }
       } else {
         intralineDifferenceIsPossible = false;
@@ -198,10 +203,10 @@
         context = diffPrefs.getContext();
         hugeFile = true;
 
-      } else if (diffPrefs.isSyntaxHighlighting()) {
-        // In order to 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.
+      } 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;
       }
@@ -212,7 +217,7 @@
         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);
+        intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
   }
 
   private static boolean isModify(PatchListEntry content) {
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/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index 2366423..7330337 100644
--- 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
@@ -14,31 +14,24 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ListBranchesResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.ListBranches.BranchInfo;
 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.project.ProjectResource;
 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.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 {
@@ -46,127 +39,37 @@
   }
 
   private final ProjectControl.Factory projectControlFactory;
-  private final GitRepositoryManager repoManager;
+  private final Provider<com.google.gerrit.server.project.ListBranches> listBranchesProvider;
 
   private final Project.NameKey projectName;
 
   @Inject
   ListBranches(final ProjectControl.Factory projectControlFactory,
-      final GitRepositoryManager repoManager,
-
+      final Provider<com.google.gerrit.server.project.ListBranches> listBranchesProvider,
       @Assisted final Project.NameKey name) {
     this.projectControlFactory = projectControlFactory;
-    this.repoManager = repoManager;
+    this.listBranchesProvider = listBranchesProvider;
 
     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;
+    ProjectControl pctl =
+        projectControlFactory.validateFor(projectName, ProjectControl.OWNER
+            | ProjectControl.VISIBLE);
     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.
-        }
+      List<Branch> branches = Lists.newArrayList();
+      List<BranchInfo> branchInfos = listBranchesProvider.get().apply(new ProjectResource(pctl));
+      for (BranchInfo info : branchInfos) {
+        Branch b = new Branch(new Branch.NameKey(projectName, info.ref));
+        b.setRevision(new RevId(info.revision));
+        b.setCanDelete(Objects.firstNonNull(info.canDelete, false));
+        branches.add(b);
       }
-
-      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();
+      return new ListBranchesResult(branches, pctl.canAddRefs(), false);
+    } catch (ResourceNotFoundException e) {
+      throw new NoSuchProjectException(projectName);
     }
-    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..ade781d 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,7 +15,6 @@
 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;
@@ -32,7 +31,6 @@
 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;
@@ -43,8 +41,7 @@
   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,
@@ -52,7 +49,6 @@
       final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
       final ProjectAccessFactory.Factory projectAccessFactory,
       final ProjectDetailFactory.Factory projectDetailFactory) {
-    this.addBranchFactory = addBranchFactory;
     this.changeProjectAccessFactory = changeProjectAccessFactory;
     this.reviewProjectAccessFactory = reviewProjectAccessFactory;
     this.changeProjectSettingsFactory = changeProjectSettingsFactory;
@@ -119,12 +115,4 @@
       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/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index 2d4f210..c809851 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,7 +28,6 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AddBranch.Factory.class);
         factory(ChangeProjectAccess.Factory.class);
         factory(ReviewProjectAccess.Factory.class);
         factory(ChangeProjectSettings.Factory.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
new file mode 100644
index 0000000..321f032
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.template;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.httpd.HtmlDomUtil;
+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 org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.File;
+import java.io.IOException;
+
+@Singleton
+public class SiteHeaderFooter {
+  private static final Logger log = LoggerFactory.getLogger(SiteHeaderFooter.class);
+
+  private final boolean refreshHeaderFooter;
+  private final SitePaths sitePaths;
+  private volatile Template template;
+
+  @Inject
+  SiteHeaderFooter(@GerritServerConfig Config cfg, SitePaths sitePaths) {
+    this.refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    this.sitePaths = sitePaths;
+
+    Template t = new Template(sitePaths);
+    try {
+      t.load();
+    } catch (IOException e) {
+      log.warn("Cannot load site header or footer", e);
+    }
+    template = t;
+  }
+
+  public Document parse(Class<?> clazz, String name) throws IOException {
+    Template t = template;
+    if (refreshHeaderFooter && t.isStale()) {
+      t = new Template(sitePaths);
+      try {
+        t.load();
+        template = t;
+      } catch (IOException e) {
+        log.warn("Cannot refresh site header or footer", e);
+        t = template;
+      }
+    }
+
+    Document doc = HtmlDomUtil.parseFile(clazz, name);
+    injectCss(doc, "gerrit_sitecss", t.css);
+    injectXml(doc, "gerrit_header", t.header);
+    injectXml(doc, "gerrit_footer", t.footer);
+    return doc;
+  }
+
+  private void injectCss(Document doc, String id, String content) {
+    Element e = HtmlDomUtil.find(doc, id);
+    if (e != null) {
+      if (!Strings.isNullOrEmpty(content)) {
+        while (e.getFirstChild() != null) {
+          e.removeChild(e.getFirstChild());
+        }
+        e.removeAttribute("id");
+        e.appendChild(doc.createCDATASection("\n" + content + "\n"));
+      } else {
+        e.getParentNode().removeChild(e);
+      }
+    }
+  }
+
+  private void injectXml(Document doc, String id, Element d) {
+    Element e = HtmlDomUtil.find(doc, id);
+    if (e != null) {
+      if (d != null) {
+        while (e.getFirstChild() != null) {
+          e.removeChild(e.getFirstChild());
+        }
+        e.appendChild(doc.importNode(d, true));
+      } else {
+        e.getParentNode().removeChild(e);
+      }
+    }
+  }
+
+  private static class Template {
+    private final FileInfo cssFile;
+    private final FileInfo headerFile;
+    private final FileInfo footerFile;
+
+    String css;
+    Element header;
+    Element footer;
+
+    Template(SitePaths site) {
+      cssFile = new FileInfo(site.site_css);
+      headerFile = new FileInfo(site.site_header);
+      footerFile = new FileInfo(site.site_footer);
+    }
+
+    void load() throws IOException {
+      css = HtmlDomUtil.readFile(
+          cssFile.path.getParentFile(),
+          cssFile.path.getName());
+      header = readXml(headerFile);
+      footer = readXml(footerFile);
+    }
+
+    boolean isStale() {
+      return cssFile.isStale() || headerFile.isStale() || footerFile.isStale();
+    }
+
+    private static Element readXml(FileInfo src) throws IOException {
+      Document d = HtmlDomUtil.parseFile(src.path);
+      return d != null ? d.getDocumentElement() : null;
+    }
+  }
+
+  private static class FileInfo {
+    final File path;
+    final long time;
+
+    FileInfo(File p) {
+      path = p;
+      time = path.lastModified();
+    }
+
+    boolean isStale() {
+      return time != path.lastModified();
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
index a4e3cca..c660311 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
@@ -29,51 +29,59 @@
         }
       })();
     </script>
+    <style id="gerrit_sitecss" type="text/css"></style>
   </head>
   <body>
-    <h2>Sign In</h2>
-    <table border="0">
-      <tr>
-        <th>Username:</th>
-        <td>
-          <form method="GET">
-            <input type="text" size="30" name="user_name" />
-            <input type="submit" value="Become Account" />
-          </form>
-        </td>
-      </tr>
+    <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+    <div id="gerrit_header"></div>
+    <div id="gerrit_body" class="gerritBody">
+      <h2>Sign In</h2>
+      <table border="0">
+        <tr>
+          <th>Username:</th>
+          <td>
+            <form method="GET">
+              <input type="text" size="30" name="user_name" />
+              <input type="submit" value="Become Account" />
+            </form>
+          </td>
+        </tr>
 
-      <tr>
-        <th>Email Address:</th>
-        <td>
-          <form method="GET">
-            <input type="text" size="30" name="preferred_email" />
-            <input type="submit" value="Become Account" />
-          </form>
-        </td>
-      </tr>
+        <tr>
+          <th>Email Address:</th>
+          <td>
+            <form method="GET">
+              <input type="text" size="30" name="preferred_email" />
+              <input type="submit" value="Become Account" />
+            </form>
+          </td>
+        </tr>
 
-      <tr>
-        <th>Account ID:</th>
-        <td>
-          <form method="GET">
-            <input type="text" size="12" name="account_id" />
-            <input type="submit" value="Become Account" />
-          </form>
-        </td>
-      </tr>
+        <tr>
+          <th>Account ID:</th>
+          <td>
+            <form method="GET">
+              <input type="text" size="12" name="account_id" />
+              <input type="submit" value="Become Account" />
+            </form>
+          </td>
+        </tr>
 
-      <tr>
-        <th>Choose:</th>
-        <td id="userlist"/>
-      </tr>
-    </table>
+        <tr>
+          <th style="vertical-align: top;">Choose:</th>
+          <td id="userlist"/>
+        </tr>
+      </table>
 
-    <hr />
-    <h2>Register</h2>
-    <form method="POST">
-      <input type="hidden" name="action" value="create_account" />
-      <input type="submit" value="New Account" />
-    </form>
+      <hr />
+      <h2>Register</h2>
+      <form method="POST">
+        <input type="hidden" name="action" value="create_account" />
+        <input type="submit" value="New Account" />
+      </form>
+    </div>
+    <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+      <div id="gerrit_footer"></div>
+    </div>
   </body>
 </html>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
index 7294012..0bc3369 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
@@ -49,22 +49,16 @@
 &lt;VirtualHost <span class='ServerName'>review.example.com</span><span class='ServerPort'>:80</span>&gt;
     ServerName <span class='ServerName'>review.example.com</span>
 
-    ProxyRequests Off
-    ProxyVia Off
-    ProxyPreserveHost On
-
-    &lt;Proxy *&gt;
-          Order deny,allow
-          Allow from all
-    &lt;/Proxy&gt;
-
 <div class='apache_auth'>    &lt;Location <span class='ContextPath'>/r</span>/login/&gt;
       AuthType Basic
       AuthName "Gerrit Code Review"
       Require valid-user
       ...
     &lt;/Location&gt;</div>
-    ProxyPass <span class='ContextPath'>/r</span>/ http://...<span class='ContextPath'>/r</span>/
+
+    AllowEncodedSlashes NoDecode
+    RewriteEngine On
+    RewriteRule ^<span class='ContextPath'>/r</span>/(.*) http://...<span class='ContextPath'>/r</span>/$1 [NE,P]
 &lt;/VirtualHost&gt;
     </pre>
   </body>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
index 72b589f..433be20 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 <html>
   <head>
     <title>Gerrit Code Review</title>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
index 986d27f..57bc7f4 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
@@ -1,7 +1,7 @@
 <html>
   <head>
     <title>Gerrit Code Review - Sign In</title>
-    <style>
+    <style type="text/css">
       #error_message {
         padding: 5px;
         margin: 2em;
@@ -13,47 +13,55 @@
         margin-left: 45px;
       }
     </style>
+    <style id="gerrit_sitecss" type="text/css"></style>
   </head>
   <body>
-    <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
-    <div id="error_message">Invalid username or password.</div>
-    <form method="POST" action="#" id="login_form">
-      <table style="border: 0;">
-      <tr>
-        <th>Username</th>
-        <td><input name="username" id="f_user"
-                   type="text"
-                   size="25"
-                   tabindex="1" /></td>
-      </tr>
-      <tr>
-        <th>Password</th>
-        <td><input name="password" id="f_pass"
-                   type="password"
-                   size="25"
-                   tabindex="2" /></td>
-      </tr>
-      <tr>
-        <td></td>
-        <td>
-          <input name="rememberme" id="f_remember"
-                 type="checkbox"
-                 value="1"
-                 tabindex="3" />
-          <label for="f_remember">Remember me</label>
-        </td>
-      </tr>
-      <tr>
-        <td></td>
-        <td>
-          <input type="submit" value="Sign In" tabindex="4"/>
-          <a href="../" id="cancel_link">Cancel</a>
-        </td>
-      </tr>
-      </table>
-    </form>
+    <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+    <div id="gerrit_header"></div>
+    <div id="gerrit_body" class="gerritBody">
+      <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
+      <div id="error_message">Invalid username or password.</div>
+      <form method="POST" action="#" id="login_form">
+        <table style="border: 0;">
+        <tr>
+          <th>Username</th>
+          <td><input name="username" id="f_user"
+                     type="text"
+                     size="25"
+                     tabindex="1" /></td>
+        </tr>
+        <tr>
+          <th>Password</th>
+          <td><input name="password" id="f_pass"
+                     type="password"
+                     size="25"
+                     tabindex="2" /></td>
+        </tr>
+        <tr>
+          <td></td>
+          <td>
+            <input name="rememberme" id="f_remember"
+                   type="checkbox"
+                   value="1"
+                   tabindex="3" />
+            <label for="f_remember">Remember me</label>
+          </td>
+        </tr>
+        <tr>
+          <td></td>
+          <td>
+            <input type="submit" value="Sign In" tabindex="4"/>
+            <a href="../" id="cancel_link">Cancel</a>
+          </td>
+        </tr>
+        </table>
+      </form>
+      <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+        <div id="gerrit_footer"></div>
+      </div>
+    </div>
 
-    <script type="text/javascript" language="javascript">
+    <script type="text/javascript">
       var login_form = document.getElementById('login_form');
       var f_user = document.getElementById('f_user');
       var f_pass = document.getElementById('f_pass');
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 907414f..ce100a5 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
@@ -31,7 +31,7 @@
       })();
     </script>
     <script id="gerrit_hostpagedata"></script>
-    <style  id="gerrit_sitecss" type="text/css"></style>
+    <style id="gerrit_sitecss" type="text/css"></style>
     <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
   </head>
   <body>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html
index 5050bf2..6639a5b 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 <html>
   <head>
     <title>Gerrit Code Review</title>
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
index 5cfde6a..7265470 100644
--- 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
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.project.RefControl;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.StandardKeyEncoder;
+import com.google.inject.Provider;
 
 import org.easymock.IExpectationSetters;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
@@ -84,6 +85,7 @@
 
     mockDb = createStrictMock(Repository.class);
     pc = createStrictMock(ProjectControl.class);
+    expect(pc.getProject()).andReturn(new Project(name)).anyTimes();
     pcf = createStrictMock(ProjectControl.Factory.class);
     grm = createStrictMock(GitRepositoryManager.class);
     refMocks = new ArrayList<RefControl>();
@@ -125,7 +127,7 @@
     validate().andThrow(err);
     doReplay();
     try {
-      new ListBranches(pcf, grm, name).call();
+      new ListBranches(pcf, createListBranchesProvider(grm), name).call();
       fail("did not throw when expected not authorized");
     } catch (NoSuchProjectException e2) {
       assertSame(err, e2);
@@ -161,7 +163,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
     assertNotNull(r.getBranches());
@@ -302,7 +305,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
 
@@ -330,7 +334,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
     assertTrue(r.getBranches().isEmpty());
@@ -359,7 +364,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
 
@@ -371,4 +377,14 @@
     assertEquals("bar", r.getBranches().get(1).getShortName());
     assertFalse(r.getBranches().get(1).getCanDelete());
   }
+
+  private static Provider<com.google.gerrit.server.project.ListBranches> createListBranchesProvider(
+      final GitRepositoryManager grm) {
+    return new Provider<com.google.gerrit.server.project.ListBranches>() {
+      @Override
+      public com.google.gerrit.server.project.ListBranches get() {
+        return new com.google.gerrit.server.project.ListBranches(grm);
+      }
+    };
+  }
 }
diff --git a/gerrit-launcher/BUCK b/gerrit-launcher/BUCK
new file mode 100644
index 0000000..8e6cb4c
--- /dev/null
+++ b/gerrit-launcher/BUCK
@@ -0,0 +1,9 @@
+java_library(
+  name = 'launcher',
+  srcs = glob(['src/main/java/**/*.java']),
+  visibility = [
+    '//gerrit-acceptance-tests:',
+    '//gerrit-main:main_lib',
+    '//gerrit-pgm:',
+  ],
+)
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 396c09c..073e0b5 100644
--- a/gerrit-launcher/pom.xml
+++ b/gerrit-launcher/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-launcher</artifactId>
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
index 25c9401..8b51cc8 100644
--- a/gerrit-main/pom.xml
+++ b/gerrit-main/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/.settings/org.eclipse.core.resources.prefs b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
index f9fe345..839d647 100644
--- a/gerrit-openid/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,5 @@
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
 encoding//src/test/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK
new file mode 100644
index 0000000..0801534
--- /dev/null
+++ b/gerrit-openid/BUCK
@@ -0,0 +1,23 @@
+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:jsr305',
+    '//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
index b232496..45df631 100644
--- a/gerrit-openid/pom.xml
+++ b/gerrit-openid/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-openid</artifactId>
@@ -51,8 +51,7 @@
 
     <dependency>
       <groupId>org.openid4java</groupId>
-      <artifactId>openid4java-consumer</artifactId>
-      <type>pom</type>
+      <artifactId>openid4java</artifactId>
     </dependency>
 
     <dependency>
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 525084f..b8d31ee 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -62,15 +63,18 @@
   private final OpenIdServiceImpl impl;
   private final int maxRedirectUrlLength;
   private final String ssoUrl;
+  private final SiteHeaderFooter header;
 
   @Inject
   LoginForm(
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       @GerritServerConfig Config config,
       AuthConfig authConfig,
-      OpenIdServiceImpl impl) {
+      OpenIdServiceImpl impl,
+      SiteHeaderFooter header) {
     this.urlProvider = urlProvider;
     this.impl = impl;
+    this.header = header;
     this.maxRedirectUrlLength = config.getInt(
         "openid", "maxRedirectUrlLength",
         10);
@@ -231,7 +235,7 @@
       cancel += "#" + token;
     }
 
-    Document doc = HtmlDomUtil.parseFile(LoginForm.class, "LoginForm.html");
+    Document doc = header.parse(LoginForm.class, "LoginForm.html");
     HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
     HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
     HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
diff --git a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
index da6eb05..f5734ffe 100644
--- a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
+++ b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
@@ -27,50 +27,58 @@
         background: #fff url('data:image/gif;base64,R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAAAAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcgEAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikDFzk0lpcjIQA7') no-repeat scroll 5px 50%
       }
     </style>
+    <style id="gerrit_sitecss" type="text/css"></style>
   </head>
   <body>
-    <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
-	<form method="POST" action="#" id="login_form">
-      <input type="hidden" name="link" id="f_link" value="1" />
-	  <div id="logo_box"><div id="logo_img"></div></div>
-      <div id="error_message">Invalid OpenID identifier.</div>
-      <div>
-        <input type="text"
-               name="id"
-               id="f_openid"
-               size="60"
-               tabindex="1" />
-      </div>
-      <div>
-        <input name="rememberme" id="f_remember"
-               type="checkbox"
-               value="1"
-               tabindex="2" />
-        <label for="f_remember">Remember me</label>
-      </div>
-      <div style="margin-bottom: 25px;">
-        <input type="submit" value="Sign In" id="f_submit" tabindex="3" />
-        <a href="../" id="cancel_link">Cancel</a>
-      </div>
+    <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+    <div id="gerrit_header"></div>
+    <div id="gerrit_body" class="gerritBody">
+      <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
+      <form method="POST" action="#" id="login_form">
+        <input type="hidden" name="link" id="f_link" value="1" />
+        <div id="logo_box"><div id="logo_img"></div></div>
+        <div id="error_message">Invalid OpenID identifier.</div>
+        <div>
+          <input type="text"
+                 name="id"
+                 id="f_openid"
+                 size="60"
+                 tabindex="1" />
+        </div>
+        <div>
+          <input name="rememberme" id="f_remember"
+                 type="checkbox"
+                 value="1"
+                 tabindex="2" />
+          <label for="f_remember">Remember me</label>
+        </div>
+        <div style="margin-bottom: 25px;">
+          <input type="submit" value="Sign In" id="f_submit" tabindex="3" />
+          <a href="../" id="cancel_link">Cancel</a>
+        </div>
 
-      <div id="provider_google">
-        <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsUlEQVR42rWSy0sbYRRH88fYhW5KQXThIkUQN6ISJFBBW6FFBN2UqlC78LVKFlHIoqWGakF8gUZ8gajVzKSKZpQmAWtK0pBmhkxmSgPuiuaYmRKhSEwU/ODyDZfzO8ydOxbLQx1NB0H4y8FBlmMpy9LyOavrKUoKB4IXvBsIIopRPJ441dY/2Fr8fAtFSxO0dkRy4eA1PDp+TkVlmGhMKS5IJOFpbYgj6fQalhV4UhVm0esvLojELimv1JiYOPwPflS2gM93WNoIXZ1nPC4/IRDQzYDTqTA0PM2dtuCZ8vGmV6SvP5srL/da5c+4TnNLFlvTMYmEVrpkazuM2x1naOQ7Dc1fqLOmaayXkaSz4hLHewm7fY+Z2R0iP+JmYGpym/q6DDb76u0CwR+g9+0FHz4u3wAdLh9VNWu5UX4VlqysfaXRDoODNz+aw7nDs9ZPpNPpwgJV1WhvF6m1/qb79T6fpwXm5kVGRnd5/mIdQSzwH2iahq7/27nxiq5xLy9fbdDWsUtPzyausRVC4VMMzmAMNv9syTdkWTZLVVUymYx5J5NJs5dKpVAUxaw8Z/SM7BV+fz9Uc4yYRAAAAABJRU5ErkJggg==" />
-        <a href="?id=https://www.google.com/accounts/o8/id" id="id_google">Sign in with a Google Account</a>
-      </div>
-      <div id="provider_yahoo">
-        <img height="16" width="16" src="data:image/gif;base64,R0lGODlhEAAQAPECAAAAAP8AAP///8zMzCH5BAEAAAMALAAAAAAQABAAAAIqnI+py30BY3AgAjCkfJDjiIAQlgUkNxqWkqrm0honKk7KhZOzw/f+fygAADs=" />
-        <a href="?id=https://me.yahoo.com" id="id_yahoo">Sign in with a Yahoo! ID</a>
-      </div>
+        <div id="provider_google">
+          <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsUlEQVR42rWSy0sbYRRH88fYhW5KQXThIkUQN6ISJFBBW6FFBN2UqlC78LVKFlHIoqWGakF8gUZ8gajVzKSKZpQmAWtK0pBmhkxmSgPuiuaYmRKhSEwU/ODyDZfzO8ydOxbLQx1NB0H4y8FBlmMpy9LyOavrKUoKB4IXvBsIIopRPJ441dY/2Fr8fAtFSxO0dkRy4eA1PDp+TkVlmGhMKS5IJOFpbYgj6fQalhV4UhVm0esvLojELimv1JiYOPwPflS2gM93WNoIXZ1nPC4/IRDQzYDTqTA0PM2dtuCZ8vGmV6SvP5srL/da5c+4TnNLFlvTMYmEVrpkazuM2x1naOQ7Dc1fqLOmaayXkaSz4hLHewm7fY+Z2R0iP+JmYGpym/q6DDb76u0CwR+g9+0FHz4u3wAdLh9VNWu5UX4VlqysfaXRDoODNz+aw7nDs9ZPpNPpwgJV1WhvF6m1/qb79T6fpwXm5kVGRnd5/mIdQSzwH2iahq7/27nxiq5xLy9fbdDWsUtPzyausRVC4VMMzmAMNv9syTdkWTZLVVUymYx5J5NJs5dKpVAUxaw8Z/SM7BV+fz9Uc4yYRAAAAABJRU5ErkJggg==" />
+          <a href="?id=https://www.google.com/accounts/o8/id" id="id_google">Sign in with a Google Account</a>
+        </div>
+        <div id="provider_yahoo">
+          <img height="16" width="16" src="data:image/gif;base64,R0lGODlhEAAQAPECAAAAAP8AAP///8zMzCH5BAEAAAMALAAAAAAQABAAAAIqnI+py30BY3AgAjCkfJDjiIAQlgUkNxqWkqrm0honKk7KhZOzw/f+fygAADs=" />
+          <a href="?id=https://me.yahoo.com" id="id_yahoo">Sign in with a Yahoo! ID</a>
+        </div>
 
-      <div style="margin-top: 25px;">
-        <h2>What is OpenID?</h2>
-        <p>OpenID provides secure single-sign-on, without revealing your passwords to this website.</p>
-        <p>There are many OpenID providers available.  You may already be member of one!</p>
-        <p><a href="http://openid.net/get/" target="_blank">Get OpenID</a></p>
-      </div>
-    </form>
+        <div style="margin-top: 25px;">
+          <h2>What is OpenID?</h2>
+          <p>OpenID provides secure single-sign-on, without revealing your passwords to this website.</p>
+          <p>There are many OpenID providers available.  You may already be member of one!</p>
+          <p><a href="http://openid.net/get/" target="_blank">Get OpenID</a></p>
+        </div>
+      </form>
+    </div>
+    <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+      <div id="gerrit_footer"></div>
+    </div>
 
-    <script type="text/javascript" language="javascript">
+    <script type="text/javascript">
       var f_openid = document.getElementById('f_openid');
       var f_submit = document.getElementById('f_submit');
       if (f_openid.value == '')
diff --git a/gerrit-package-plugins/.gitignore b/gerrit-package-plugins/.gitignore
deleted file mode 100644
index c96b05c..0000000
--- a/gerrit-package-plugins/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-/target
-/.classpath
-/.project
-/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
-/gerrit-package-plugins.iml
diff --git a/gerrit-package-plugins/pom.xml b/gerrit-package-plugins/pom.xml
deleted file mode 100644
index f7fd22c..0000000
--- a/gerrit-package-plugins/pom.xml
+++ /dev/null
@@ -1,108 +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>
-
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-package-plugins</artifactId>
-  <packaging>war</packaging>
-  <version>2.7-SNAPSHOT</version>
-
-  <name>Gerrit Code Review - Package Plugins</name>
-  <url>http://code.google.com/p/gerrit/</url>
-
-  <properties>
-    <project.build.sourceEncoding>
-      UTF-8
-    </project.build.sourceEncoding>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-war</artifactId>
-      <version>${project.version}</version>
-      <type>war</type>
-    </dependency>
-    <dependency>
-      <groupId>com.googlesource.gerrit.plugins.replication</groupId>
-      <artifactId>replication</artifactId>
-      <version>1.1-rc0</version>
-      <scope>provided</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.googlesource.gerrit.plugins.reviewnotes</groupId>
-      <artifactId>reviewnotes</artifactId>
-      <version>1.0-rc0</version>
-      <scope>provided</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.googlesource.gerrit.plugins.validators</groupId>
-      <artifactId>commit-message-length-validator</artifactId>
-      <version>1.0-rc0</version>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-dependency-plugin</artifactId>
-        <version>2.1</version>
-        <executions>
-          <execution>
-            <phase>process-resources</phase>
-            <goals>
-              <goal>copy-dependencies</goal>
-            </goals>
-            <configuration>
-              <includeTypes>jar</includeTypes>
-              <stripVersion>true</stripVersion>
-              <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/plugins</outputDirectory>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-war-plugin</artifactId>
-        <version>2.1.1</version>
-        <configuration>
-          <warName>gerrit-full-${project.version}</warName>
-          <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>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
-  <repositories>
-    <repository>
-      <id>gerrit-plugins</id>
-      <url>https://gerrit-plugins.commondatastorage.googleapis.com</url>
-    </repository>
-  </repositories>
-</project>
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
index 7718721..2b4874f 100644
--- a/gerrit-patch-commonsnet/pom.xml
+++ b/gerrit-patch-commonsnet/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
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
index 222d622..dc60e1c 100644
--- a/gerrit-patch-jgit/pom.xml
+++ b/gerrit-patch-jgit/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
new file mode 100644
index 0000000..0746bdf
--- /dev/null
+++ b/gerrit-pgm/BUCK
@@ -0,0 +1,58 @@
+java_library2(
+  name = 'pgm',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/resources/**/*']),
+  deps = [
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-gwtexpui:server',
+    '//gerrit-httpd:httpd',
+    '//gerrit-openid:openid',
+    '//gerrit-server:common_rules',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-sshd:sshd',
+    '//gerrit-util-cli:cli',
+    '//lib:args4j',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:gwtjsonrpc',
+    '//lib:h2',
+    '//lib:jsr305',
+    '//lib:servlet-api-3_0',
+    '//lib/commons:dbcp',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+    '//lib/jetty:server',
+    '//lib/jetty:servlet',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+    '//lib/log:log4j',
+    '//lib/mina:sshd',
+    '//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 = [
+    ':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
index c7121c7a..e9d4bd5 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
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 c0f0c4b..3c7822c 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
@@ -21,7 +21,6 @@
 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.ReloadSiteLibrary;
 import com.google.gerrit.pgm.init.SitePathInitializer;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.pgm.util.Die;
@@ -121,12 +120,6 @@
       protected void configure() {
         bind(ConsoleUI.class).toInstance(ui);
         bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
-        bind(ReloadSiteLibrary.class).toInstance(new ReloadSiteLibrary() {
-          @Override
-          public void reload() {
-            Init.super.loadSiteLib();
-          }
-        });
       }
     });
 
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..b792f68
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.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.pgm.http.jetty;
+
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+
+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.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);
+  private static final byte[] MSG = "Internal server error\n".getBytes(Charsets.ISO_8859_1);
+
+  public void handle(String target, Request baseRequest,
+      HttpServletRequest req, HttpServletResponse res) throws IOException {
+    AbstractHttpConnection.getCurrentConnection().getRequest().setHandled(true);
+    try {
+      log(req);
+    } finally {
+      replyGenericError(res);
+    }
+  }
+
+  private void replyGenericError(HttpServletResponse res) throws IOException {
+    if (!res.isCommitted()) {
+      res.reset();
+      res.setStatus(SC_INTERNAL_SERVER_ERROR);
+      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 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/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 3a7a874..37637d5 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,6 +17,8 @@
 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;
@@ -49,12 +51,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,6 +70,7 @@
 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;
@@ -72,6 +79,8 @@
 
 @Singleton
 public class JettyServer {
+  private static final Logger log = LoggerFactory.getLogger(JettyServer.class);
+
   static class Lifecycle implements LifecycleListener {
     private final JettyServer server;
 
@@ -323,6 +332,7 @@
     // 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.
     //
@@ -368,7 +378,7 @@
   private Resource getBaseResource() throws IOException {
     if (baseResource == null) {
       try {
-        baseResource = unpackWar();
+        baseResource = unpackWar(GerritLauncher.getDistributionArchive());
       } catch (FileNotFoundException err) {
         if (err.getMessage() == GerritLauncher.NOT_ARCHIVED) {
           baseResource = useDeveloperBuild();
@@ -380,9 +390,7 @@
     return baseResource;
   }
 
-  private Resource unpackWar() throws IOException {
-    final File srcwar = GerritLauncher.getDistributionArchive();
-
+  private Resource unpackWar(File srcwar) 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.
     //
@@ -474,15 +482,87 @@
       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())) {
+      String pkg = "gerrit-gwtui";
+      String target = targetForBrowser(System.getProperty("gerrit.browser"));
+      File gen = new File(dir, "gen");
+      String out = new File(new File(gen, pkg), target).getAbsolutePath();
+      build(dir.getParentFile(), gen, "//" + pkg + ":" + target);
+      return unpackWar(new File(out + ".zip"));
+    } 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.contains("PATH")) {
+      proc.environment().put("PATH", properties.getProperty("PATH"));
+    }
+    long start = System.currentTimeMillis();
+    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 = System.currentTimeMillis() - 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/init/Browser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
index ea13043..8e3948e 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 
@@ -42,7 +43,6 @@
     if (url == null) {
       return;
     }
-
     if (url.startsWith("proxy-")) {
       url = url.substring("proxy-".length());
     }
@@ -54,15 +54,19 @@
       System.err.println("error: invalid httpd.listenUrl: " + url);
       return;
     }
-    final String hostname = uri.getHost();
-    final int port = InitUtil.portOf(uri);
+    waitForServer(uri);
+    openBrowser(uri, link);
+  }
 
-    System.err.print("Waiting for server to start ... ");
+  private void waitForServer(URI uri) throws IOException {
+    String host = uri.getHost();
+    int port = InitUtil.portOf(uri);
+    System.err.format("Waiting for server on %s:%d ... ", host, port);
     System.err.flush();
     for (;;) {
-      final Socket s;
+      Socket s;
       try {
-        s = new Socket(hostname, port);
+        s = new Socket(host, port);
       } catch (IOException e) {
         try {
           Thread.sleep(100);
@@ -74,18 +78,33 @@
       break;
     }
     System.err.println("OK");
+  }
 
-    url = cfg.getString("gerrit", null, "canonicalWebUrl");
-    if (url == null || url.isEmpty()) {
+  private String resolveUrl(URI uri, String link) {
+    String url = cfg.getString("gerrit", null, "canonicalWebUrl");
+    if (Strings.isNullOrEmpty(url)) {
       url = uri.toString();
     }
     if (!url.endsWith("/")) {
       url += "/";
     }
-    if (link != null && !link.isEmpty()) {
+    if (!Strings.isNullOrEmpty(link)) {
       url += "#" + link;
     }
-    System.err.println("Opening browser ...");
-    org.h2.tools.Server.openBrowser(url);
+    return url;
+  }
+
+  private void openBrowser(URI uri, String link) {
+    String url = resolveUrl(uri, link);
+    System.err.format("Opening %s ...", url);
+    System.err.flush();
+    try {
+      org.h2.tools.Server.openBrowser(url);
+      System.err.println("OK");
+    } catch (Exception e) {
+      System.err.println("FAILED");
+      System.err.println("Open Gerrit with a JavaScript capable browser:");
+      System.err.println("  " + url);
+    }
   }
 }
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 4ce963a..aa413d6 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
@@ -16,6 +16,8 @@
 
 import static com.google.inject.Stage.PRODUCTION;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Binding;
@@ -31,7 +33,6 @@
 import java.lang.annotation.Annotation;
 import java.util.List;
 import java.util.Set;
-import java.util.TreeSet;
 
 /** Initialize the {@code database} configuration section. */
 @Singleton
@@ -53,7 +54,7 @@
   public void run() {
     ui.header("SQL Database");
 
-    Set<String> allowedValues = new TreeSet<String>();
+    Set<String> allowedValues = Sets.newTreeSet();
     Injector i = Guice.createInjector(PRODUCTION, new DatabaseConfigModule(site));
     List<Binding<DatabaseConfigInitializer>> dbConfigBindings =
         i.findBindingsByType(new TypeLiteral<DatabaseConfigInitializer>() {});
@@ -64,6 +65,11 @@
       }
     }
 
+    if (!Strings.isNullOrEmpty(database.get("url"))
+        && Strings.isNullOrEmpty(database.get("type"))) {
+      database.set("type", "jdbc");
+    }
+
     String dbType =
         database.select("Database server type", "type", "h2", allowedValues);
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
index 94425ac1..20034c1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
@@ -16,13 +16,29 @@
 
 import static com.google.gerrit.pgm.init.InitUtil.username;
 
-class JDBCInitializer implements DatabaseConfigInitializer {
+import com.google.common.base.Strings;
 
+class JDBCInitializer implements DatabaseConfigInitializer {
   @Override
-  public void initConfig(Section databaseSection) {
-    databaseSection.string("Driver class name", "driver", null);
-    databaseSection.string("URL", "url", null);
-    databaseSection.string("Database username", "username", username());
-    databaseSection.password("username", "password");
+  public void initConfig(Section database) {
+    boolean hasUrl = Strings.emptyToNull(database.get("url")) != null;
+    database.string("URL", "url", null);
+    guessDriver(database);
+    database.string("Driver class name", "driver", null);
+    database.string("Database username", "username", hasUrl ? null : username());
+    database.password("username", "password");
+  }
+
+  private void guessDriver(Section database) {
+    String url = Strings.emptyToNull(database.get("url"));
+    if (url != null && Strings.isNullOrEmpty(database.get("driver"))) {
+      if (url.startsWith("jdbc:h2:")) {
+        database.set("driver", "org.h2.Driver");
+      } else if (url.startsWith("jdbc:mysql:")) {
+        database.set("driver", "com.mysql.jdbc.Driver");
+      } else if (url.startsWith("jdbc:postgresql:")) {
+        database.set("driver", "org.postgresql.Driver");
+      }
+    }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index ff1eddf..b1fa0c3 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
@@ -78,6 +78,7 @@
     dl.setName(get(cfg, n, "name"));
     dl.setJarUrl(get(cfg, n, "url"));
     dl.setSHA1(get(cfg, n, "sha1"));
+    dl.setRemove(get(cfg, n, "remove"));
     field.set(this, dl);
   }
 
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 ea1b515..bf358f4 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
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.pgm.util.Die;
+import com.google.gerrit.pgm.util.IoUtil;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 
@@ -26,6 +28,7 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -40,20 +43,18 @@
 class LibraryDownloader {
   private final ConsoleUI ui;
   private final File lib_dir;
-  private final ReloadSiteLibrary reload;
 
   private boolean required;
   private String name;
   private String jarUrl;
   private String sha1;
+  private String remove;
   private File dst;
 
   @Inject
-  LibraryDownloader(final ReloadSiteLibrary reload, final ConsoleUI ui,
-      final SitePaths site) {
+  LibraryDownloader(ConsoleUI ui, SitePaths site) {
     this.ui = ui;
     this.lib_dir = site.lib_dir;
-    this.reload = reload;
   }
 
   void setName(final String name) {
@@ -68,6 +69,10 @@
     this.sha1 = sha1;
   }
 
+  void setRemove(String remove) {
+    this.remove = remove;
+  }
+
   void downloadRequired() {
     this.required = true;
     download();
@@ -123,6 +128,7 @@
     }
 
     try {
+      removeStaleVersions();
       doGetByHttp();
       verifyFileChecksum();
     } catch (IOException err) {
@@ -155,7 +161,29 @@
       }
     }
 
-    reload.reload();
+    if (dst.exists()) {
+      IoUtil.loadJARs(dst);
+    }
+  }
+
+  private void removeStaleVersions() {
+    if (!Strings.isNullOrEmpty(remove)) {
+      String[] names = lib_dir.list(new FilenameFilter() {
+        @Override
+        public boolean accept(File dir, String name) {
+          return name.matches("^" + remove + "$");
+        }
+      });
+      if (names != null) {
+        for (String old : names) {
+          String bak = "." + old + ".backup";
+          ui.message("Renaming %s to %s", old, bak);
+          if (!new File(lib_dir, old).renameTo(new File(lib_dir, bak))) {
+            throw new Die("cannot rename " + old);
+          }
+        }
+      }
+    }
   }
 
   private void doGetByHttp() throws IOException {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
index 9398851..e28af7c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
@@ -14,9 +14,19 @@
 
 package com.google.gerrit.pgm.util;
 
+import com.google.common.collect.Sets;
+
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
+import java.util.Set;
 
 public final class IoUtil {
   public static void copyWithThread(final InputStream src,
@@ -42,6 +52,46 @@
     }.start();
   }
 
+  public static void loadJARs(File... jars) {
+    ClassLoader cl = IoUtil.class.getClassLoader();
+    if (!(cl instanceof URLClassLoader)) {
+      throw noAddURL("Not loaded by URLClassLoader", null);
+    }
+    URLClassLoader urlClassLoader = (URLClassLoader) cl;
+
+    Method addURL;
+    try {
+      addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
+      addURL.setAccessible(true);
+    } catch (SecurityException e) {
+      throw noAddURL("Method addURL not available", e);
+    } catch (NoSuchMethodException e) {
+      throw noAddURL("Method addURL not available", e);
+    }
+
+    Set<URL> have = Sets.newHashSet(Arrays.asList(urlClassLoader.getURLs()));
+    for (File path : jars) {
+      try {
+        URL url = path.toURI().toURL();
+        if (have.add(url)) {
+          addURL.invoke(cl, url);
+        }
+      } catch (MalformedURLException e) {
+        throw noAddURL("addURL " + path + " failed", e);
+      } catch (IllegalArgumentException e) {
+        throw noAddURL("addURL " + path + " failed", e);
+      } catch (IllegalAccessException e) {
+        throw noAddURL("addURL " + path + " failed", e);
+      } catch (InvocationTargetException e) {
+        throw noAddURL("addURL " + path + " failed", e.getCause());
+      }
+    }
+  }
+
+  private static UnsupportedOperationException noAddURL(String m, Throwable why) {
+    String prefix = "Cannot extend classpath: ";
+    return new UnsupportedOperationException(prefix + m, why);
+  }
   private IoUtil() {
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
new file mode 100644
index 0000000..6ab7395
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.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.pgm.util;
+
+import com.google.common.primitives.Longs;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.schema.DataSourceType;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.Arrays;
+import java.util.Comparator;
+
+import javax.sql.DataSource;
+
+/** Loads the site library if not yet loaded. */
+@Singleton
+public class SiteLibraryBasedDataSourceProvider extends DataSourceProvider {
+  private final File libdir;
+  private boolean init;
+
+  @Inject
+  SiteLibraryBasedDataSourceProvider(SitePaths site,
+      @GerritServerConfig Config cfg,
+      DataSourceProvider.Context ctx,
+      DataSourceType dst) {
+    super(site, cfg, ctx, dst);
+    libdir = site.lib_dir;
+  }
+
+  public synchronized DataSource get() {
+    if (!init) {
+      loadSiteLib();
+      init = true;
+    }
+    return super.get();
+  }
+
+  private void loadSiteLib() {
+    File[] jars = libdir.listFiles(new FileFilter() {
+      @Override
+      public boolean accept(File path) {
+        String name = path.getName();
+        return (name.endsWith(".jar") || name.endsWith(".zip"))
+            && path.isFile();
+      }
+    });
+    if (jars != null && 0 < jars.length) {
+      Arrays.sort(jars, new Comparator<File>() {
+        @Override
+        public int compare(File a, File b) {
+          // Sort by reverse last-modified time so newer JARs are first.
+          int cmp = Longs.compare(b.lastModified(), a.lastModified());
+          if (cmp != 0) {
+            return cmp;
+          }
+          return a.getName().compareTo(b.getName());
+        }
+      });
+      IoUtil.loadJARs(jars);
+    }
+  }
+}
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 242ca28..aae5b48 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
@@ -41,19 +41,9 @@
 import org.kohsuke.args4j.Option;
 
 import java.io.File;
-import java.io.FileFilter;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
 import java.sql.SQLException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import javax.sql.DataSource;
 
@@ -78,77 +68,8 @@
     }
   }
 
-  /** Load extra JARs from {@code lib/} subdirectory of {@link #getSitePath()} */
-  protected void loadSiteLib() {
-    final File libdir = new File(getSitePath(), "lib");
-    final File[] list = libdir.listFiles(new FileFilter() {
-      @Override
-      public boolean accept(File path) {
-        if (!path.isFile()) {
-          return false;
-        }
-        return path.getName().endsWith(".jar") //
-            || path.getName().endsWith(".zip");
-      }
-    });
-    if (list != null && 0 < list.length) {
-      Arrays.sort(list, new Comparator<File>() {
-        @Override
-        public int compare(File a, File b) {
-          return a.getName().compareTo(b.getName());
-        }
-      });
-      addToClassLoader(list);
-    }
-  }
-
-  private void addToClassLoader(final File[] additionalLocations) {
-    final ClassLoader cl = getClass().getClassLoader();
-    if (!(cl instanceof URLClassLoader)) {
-      throw noAddURL("Not loaded by URLClassLoader", null);
-    }
-
-    final URLClassLoader ucl = (URLClassLoader) cl;
-    final Set<URL> have = new HashSet<URL>();
-    have.addAll(Arrays.asList(ucl.getURLs()));
-
-    final Method m;
-    try {
-      m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
-      m.setAccessible(true);
-    } catch (SecurityException e) {
-      throw noAddURL("Method addURL not available", e);
-    } catch (NoSuchMethodException e) {
-      throw noAddURL("Method addURL not available", e);
-    }
-
-    for (final File path : additionalLocations) {
-      try {
-        final URL url = path.toURI().toURL();
-        if (have.add(url)) {
-          m.invoke(cl, url);
-        }
-      } catch (MalformedURLException e) {
-        throw noAddURL("addURL " + path + " failed", e);
-      } catch (IllegalArgumentException e) {
-        throw noAddURL("addURL " + path + " failed", e);
-      } catch (IllegalAccessException e) {
-        throw noAddURL("addURL " + path + " failed", e);
-      } catch (InvocationTargetException e) {
-        throw noAddURL("addURL " + path + " failed", e.getCause());
-      }
-    }
-  }
-
-  private static UnsupportedOperationException noAddURL(String m, Throwable why) {
-    final String prefix = "Cannot extend classpath: ";
-    return new UnsupportedOperationException(prefix + m, why);
-  }
-
   /** @return provides database connectivity and site path. */
   protected Injector createDbInjector(final DataSourceProvider.Context context) {
-    loadSiteLib();
-
     final File sitePath = getSitePath();
     final List<Module> modules = new ArrayList<Module>();
 
@@ -164,9 +85,10 @@
       @Override
       protected void configure() {
         bind(DataSourceProvider.Context.class).toInstance(context);
-        bind(Key.get(DataSource.class, Names.named("ReviewDb"))).toProvider(
-            DataSourceProvider.class).in(SINGLETON);
-        listener().to(DataSourceProvider.class);
+        bind(Key.get(DataSource.class, Names.named("ReviewDb")))
+          .toProvider(SiteLibraryBasedDataSourceProvider.class)
+          .in(SINGLETON);
+        listener().to(SiteLibraryBasedDataSourceProvider.class);
       }
     });
     Module configModule = new GerritServerConfigModule();
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 f4c5808..a5150e6 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,12 +13,15 @@
 # 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
   sha1 = 6327a5f7a3dc45e0fd735adb5d08c5a74c05c20c
+  remove = bcprov-.*[.]jar
 
 [library "mysqlDriver"]
   name = MySQL Connector/J 5.1.21
   url = http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar
   sha1 = 7abbd19fc2e2d5b92c0895af8520f7fa30266be9
+  remove = mysql-connector-java-.*[.]jar
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
index e723463..df1f447 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
@@ -30,16 +30,14 @@
 public class LibrariesTest extends TestCase {
   public void testCreate() throws FileNotFoundException {
     final SitePaths site = new SitePaths(new File("."));
-    final ReloadSiteLibrary reload = createStrictMock(ReloadSiteLibrary.class);
     final ConsoleUI ui = createStrictMock(ConsoleUI.class);
 
     replay(ui);
-    replay(reload);
 
     Libraries lib = new Libraries(new Provider<LibraryDownloader>() {
       @Override
       public LibraryDownloader get() {
-        return new LibraryDownloader(reload, ui, site);
+        return new LibraryDownloader(ui, site);
       }
     });
 
@@ -47,6 +45,5 @@
     assertNotNull(lib.mysqlDriver);
 
     verify(ui);
-    verify(reload);
   }
 }
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 0abe81f..dd5a684 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-plugin-api</artifactId>
@@ -72,8 +72,8 @@
           <createSourcesJar>true</createSourcesJar>
           <artifactSet>
             <excludes>
-              <exclude>gwtexpui:gwtexpui</exclude>
               <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>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 9b954f9..f1d6b4d 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-plugin-archetype</artifactId>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 66f5a8f..65db8c5 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
index a88059d..73bf5c6 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
@@ -6,7 +6,7 @@
  */
 
 body, table td, select {
-  font-family: Arial Unicode MS, Arial, sans-serif;
+  font-family: sans-serif;
   font-size: small;
 }
 pre {
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 5195204..01c3915 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-plugin-gwtui</artifactId>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index 5b12f7d..5e31d90 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-plugin-js-archetype</artifactId>
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
index 2479adc..4c7f9f1 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
@@ -34,8 +34,9 @@
 
   <dependencies>
     <dependency>
-      <groupId>gwtexpui</groupId>
-      <artifactId>gwtexpui</artifactId>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-gwtexpui</artifactId>
+      <version>${project.version}</version>
     </dependency>
 
     <dependency>
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 1436bba..9f76a47 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
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.prettify.client;
 
-import com.google.gerrit.prettify.common.PrettyFactory;
-import com.google.gerrit.prettify.common.PrettyFormatter;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.ui.RootPanel;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
similarity index 95%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
index df60305..c191fa5 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.i18n.client.Constants;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
similarity index 100%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
similarity index 94%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
index 364789f..f68b629 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
 /** Creates a new PrettyFormatter instance for one formatting run. */
 public interface PrettyFactory {
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
similarity index 99%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index 511b056..2836f33 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
+import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
similarity index 95%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
index ebe0855..0c2af36 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
 
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index 609f091..aa08af0 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.prettify.common;
 
+
 import org.eclipse.jgit.diff.Edit;
 
 import java.util.ArrayList;
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
index d69c5c5..4cef8cc 100644
--- a/gerrit-reviewdb/pom.xml
+++ b/gerrit-reviewdb/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
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 80ea82f..94b37e1 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
@@ -169,7 +169,11 @@
 
   /** Set the full name of the user ("Given-name Surname" style). */
   public void setFullName(final String name) {
-    fullName = name != null ? name.trim() : null;
+    if (name != null && !name.trim().isEmpty()) {
+      fullName = name.trim();
+    } else {
+      fullName = null;
+    }
   }
 
   /** Email address the user prefers to be contacted through. */
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 c070e3e..d243496 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
@@ -113,6 +113,8 @@
 
   protected String localDefaultDashboardId;
 
+  protected String themeName;
+
   protected Project() {
   }
 
@@ -206,6 +208,14 @@
     this.localDefaultDashboardId = localDefaultDashboardId;
   }
 
+  public String getThemeName() {
+    return themeName;
+  }
+
+  public void setThemeName(final String themeName) {
+    this.themeName = themeName;
+  }
+
   public void copySettingsFrom(final Project update) {
     description = update.description;
     useContributorAgreements = update.useContributorAgreements;
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..1d676a0
--- /dev/null
+++ b/gerrit-server/BUCK
@@ -0,0 +1,85 @@
+include_defs('//lib/prolog/DEFS')
+
+# TODO(sop) break up gerrit-server java_library(), its too big
+java_library2(
+  name = 'server',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/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:jsr305',
+    '//lib:juniversalchardet',
+    '//lib:mime-util',
+    '//lib:ow2-asm',
+    '//lib:ow2-asm-tree',
+    '//lib:ow2-asm-util',
+    '//lib:parboiled-core',
+    '//lib:pegdown',
+    '//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/log:api',
+    '//lib/prolog:prolog-cafe',
+  ],
+  compile_deps = [
+    '//lib/bouncycastle:bcprov',
+    '//lib/bouncycastle:bcpg',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+prolog_cafe_library(
+  name = 'common_rules',
+  srcs = ['src/main/prolog/gerrit_common.pl'],
+  deps = [':server'],
+  visibility = ['PUBLIC'],
+)
+
+java_test(
+  name = 'server_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  resources = glob(['src/test/resources/**/*']),
+  deps = [
+    ':server',
+    ':common_rules',
+    '//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:h2',
+    '//lib:junit',
+    '//lib/antlr:java_runtime',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/jgit:junit',
+    '//lib/prolog:prolog-cafe',
+  ],
+  source_under_test = [':server'],
+)
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 68b1625..04b053c 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
@@ -234,6 +234,14 @@
 
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <argLine>-Djava.io.tmpdir=${project.build.directory}</argLine>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-source-plugin</artifactId>
         <executions>
           <execution>
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 4116633..2d54601 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
@@ -32,7 +32,7 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.events.ApprovalAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
 import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.events.ChangeMergedEvent;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
deleted file mode 100644
index 6e635cb..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
+++ /dev/null
@@ -1,43 +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;
-
-import java.util.Collection;
-
-/** Utilities for manipulating Collections . */
-public class CollectionsUtil {
-  /**
-   * Checks if any of the elements in the first collection can be found in the
-   * second collection.
-   *
-   * @param findAnyOfThese which elements to look for.
-   * @param inThisCollection where to look for them.
-   * @param <E> type of the elements in question.
-   * @return {@code true} if any of the elements in {@code findAnyOfThese} can
-   *         be found in {@code inThisCollection}, {@code false} otherwise.
-   */
-  public static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese,
-      Collection<E> inThisCollection) {
-    for (E findThisItem : findAnyOfThese) {
-      if (inThisCollection.contains(findThisItem)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private CollectionsUtil() {
-  }
-}
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 76c33ab..3fd24f1 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
@@ -64,29 +64,39 @@
   }
 
   /**
-   * Moves the PatchSetApprovals to the specified PatchSet on the change from
-   * the prior PatchSet, while keeping the vetos.
+   * Copy min/max scores from one patch set to another.
    *
    * @throws OrmException
-   * @return List<PatchSetApproval> The previous approvals
    */
-  public static List<PatchSetApproval> copyLabels(ReviewDb db,
-      LabelTypes labelTypes,
-      PatchSet.Id source,
+  public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
+      PatchSet.Id source, PatchSet.Id dest) throws OrmException {
+    Iterable<PatchSetApproval> sourceApprovals =
+        db.patchSetApprovals().byPatchSet(source);
+    copyLabels(db, labelTypes, sourceApprovals, source, dest);
+  }
+
+  /**
+   * Copy a set's min/max scores from one patch set to another.
+   *
+   * @throws OrmException
+   */
+  public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
+      Iterable<PatchSetApproval> sourceApprovals, PatchSet.Id source,
       PatchSet.Id dest) throws OrmException {
     List<PatchSetApproval> copied = Lists.newArrayList();
-    for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(source)) {
-      LabelType type = labelTypes.byLabel(a.getLabelId());
-      if (type == null) {
-        continue;
-      } else if (type.isCopyMinScore() && type.isMaxNegative(a)) {
-        copied.add(new PatchSetApproval(dest, a));
-      } else if (type.isCopyMaxScore() && type.isMaxPositive(a)) {
-        copied.add(new PatchSetApproval(dest, a));
+    for (PatchSetApproval a : sourceApprovals) {
+      if (source.equals(a.getPatchSetId())) {
+        LabelType type = labelTypes.byLabel(a.getLabelId());
+        if (type == null) {
+          continue;
+        } else if (type.isCopyMinScore() && type.isMaxNegative(a)) {
+          copied.add(new PatchSetApproval(dest, a));
+        } else if (type.isCopyMaxScore() && type.isMaxPositive(a)) {
+          copied.add(new PatchSetApproval(dest, a));
+        }
       }
     }
     db.patchSetApprovals().insert(copied);
-    return copied;
   }
 
   public void addReviewers(ReviewDb db, LabelTypes labelTypes, Change change,
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 a1dbd54..f4a7f73 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
@@ -16,7 +16,9 @@
 
 import com.google.common.base.CharMatcher;
 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;
@@ -25,7 +27,9 @@
 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;
@@ -43,7 +47,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 
@@ -199,9 +202,9 @@
       IdentifiedUser user, CommitValidators commitValidators, String message,
       ReviewDb db, RevertedSender.Factory revertedSenderFactory,
       ChangeHooks hooks, Repository git,
-      PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
-      String canonicalWebUrl) throws NoSuchChangeException, EmailException,
+      PatchSetInfoFactory patchSetInfoFactory, PersonIdent myIdent,
+      ChangeInserter changeInserter)
+          throws NoSuchChangeException, EmailException,
       OrmException, MissingObjectException, IncorrectObjectTypeException,
       IOException, InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
@@ -226,7 +229,7 @@
       revertCommitBuilder.addParentId(commitToRevert);
       revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
       revertCommitBuilder.setAuthor(authorIdent);
-      revertCommitBuilder.setCommitter(myIdent);
+      revertCommitBuilder.setCommitter(authorIdent);
 
       if (message == null) {
         message = MessageFormat.format(
@@ -275,9 +278,11 @@
         throw new InvalidChangeOperationException(e.getMessage());
       }
 
-      change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId()));
+      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);
@@ -287,17 +292,6 @@
             "Failed to create ref %s in %s: %s", ps.getRefName(),
             change.getDest().getParentKey().get(), ru.getResult()));
       }
-      gitRefUpdated.fire(change.getProject(), ru);
-
-      db.changes().beginTransaction(change.getId());
-      try {
-        insertAncestors(db, ps.getId(), revertCommit);
-        db.patchSets().insert(Collections.singleton(ps));
-        db.changes().insert(Collections.singleton(change));
-        db.commit();
-      } finally {
-        db.rollback();
-      }
 
       final ChangeMessage cmsg =
           new ChangeMessage(new ChangeMessage.Key(changeId,
@@ -306,17 +300,17 @@
           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());
-      db.changeMessages().insert(Collections.singleton(cmsg));
+
+      LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
+      changeInserter.insertChange(db, change, cmsg, ps, revertCommit,
+          labelTypes, info, Collections.<Account.Id> emptySet());
 
       final RevertedSender cm = revertedSenderFactory.create(change);
       cm.setFrom(user.getAccountId());
       cm.setChangeMessage(cmsg);
       cm.send();
 
-      hooks.doPatchsetCreatedHook(change, ps, db);
-
       return change.getId();
     } finally {
       revWalk.release();
@@ -327,10 +321,8 @@
       final RefControl refControl, CommitValidators commitValidators,
       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, final GitReferenceUpdated gitRefUpdated,
+      PersonIdent myIdent, PatchSetInserter patchSetInserter)
       throws NoSuchChangeException, EmailException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, PatchSetInfoNotAvailableException {
@@ -341,15 +333,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();
@@ -381,9 +376,6 @@
       newPatchSet.setRevision(new RevId(newCommit.name()));
       newPatchSet.setDraft(originalPS.isDraft());
 
-      final PatchSetInfo info =
-          patchSetInfoFactory.get(newCommit, newPatchSet.getId());
-
       CommitReceivedEvent commitReceivedEvent =
           new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
               newCommit.getId(), newPatchSet.getRefName()), refControl
@@ -407,69 +399,13 @@
       }
       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()));
-        }
+      final String msg =
+          "Patch Set " + newPatchSet.getPatchSetId()
+              + ": Commit message was updated";
 
-        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 =
+          patchSetInserter.insertPatchSet(change, newPatchSet, newCommit,
+              refControl, msg, true);
 
       return change.getId();
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index f13eee9..6f5618b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -31,6 +31,8 @@
  * anything it wants, anytime it wants, given the JVM's own direct access to
  * data. Plugins may use this when they need to have a CurrentUser with read
  * permission on anything.
+ *
+ * @see PluginUser
  */
 public class InternalUser extends CurrentUser {
   public interface Factory {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
index 792c1e7..b271d6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
@@ -60,6 +60,38 @@
     mimeUtil.registerMimeDetector(name);
   }
 
+
+  /**
+   * Get specificity of mime types with generic types forced to low values
+   *
+   * "application/octet-stream" is forced to -1.
+   * "text/plain" is forced to 0.
+   * All other mime types return the specificity reported by mimeType itself.
+   *
+   * @param mimeType The mimeType to get the corrected specificity for.
+   * @return The corrected specificity.
+   */
+  private int getCorrectedMimeSpecificity(MimeType mimeType) {
+    // Although the documentation of MimeType's getSpecificity claims that for
+    // example "application/octet-stream" always has a specificity of 0, it
+    // effectively returns 1 for us. This causes problems when trying to get
+    // the correct mime type via sorting. For example in
+    // [application/octet-stream, image/x-icon] both mime types come with
+    // specificity 1 for us. Hence, getMimeType below may end up using
+    // application/octet-stream instead of the more specific image/x-icon.
+    // Therefore, we have to force the specificity of generic types below the
+    // default of 1.
+    //
+    final String mimeTypeStr = mimeType.toString();
+    if (mimeTypeStr.equals("application/octet-stream")) {
+      return -1;
+    }
+    if (mimeTypeStr.equals("text/plain")) {
+      return 0;
+    }
+    return mimeType.getSpecificity();
+  }
+
   @SuppressWarnings("unchecked")
   public MimeType getMimeType(final String path, final byte[] content) {
     Set<MimeType> mimeTypes = new HashSet<MimeType>();
@@ -84,7 +116,7 @@
     Collections.sort(types, new Comparator<MimeType>() {
       @Override
       public int compare(MimeType a, MimeType b) {
-        return b.getSpecificity() - a.getSpecificity();
+        return getCorrectedMimeSpecificity(b) - getCorrectedMimeSpecificity(a);
       }
     });
     return types.get(0);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
new file mode 100644
index 0000000..24d10f7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.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;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+/** Utilities for option parsing. */
+public class OptionUtil {
+  private static final Splitter COMMA_OR_SPACE =
+      Splitter.on(CharMatcher.anyOf(", ")).omitEmptyStrings().trimResults();
+
+  private static final Function<String, String> TO_LOWER_CASE =
+      new Function<String, String>() {
+        @Override
+        public String apply(String input) {
+          return input.toLowerCase();
+        }
+      };
+
+  public static Iterable<String> splitOptionValue(String value) {
+    return Iterables.transform(COMMA_OR_SPACE.split(value), TO_LOWER_CASE);
+  }
+
+  private OptionUtil() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java
new file mode 100644
index 0000000..490ab07
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.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;
+
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** User identity for plugin code that needs an identity. */
+public class PluginUser extends InternalUser {
+  public interface Factory {
+    PluginUser create(String pluginName);
+  }
+
+  private final String pluginName;
+
+  @Inject
+  protected PluginUser(
+      CapabilityControl.Factory capabilityControlFactory,
+      @Assisted String pluginName) {
+    super(capabilityControlFactory);
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public String getUserName() {
+    return "plugin " + pluginName;
+  }
+
+  @Override
+  public String toString() {
+    return "PluginUser[" + pluginName + "]";
+  }
+}
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..2f95368 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
@@ -112,12 +112,14 @@
   public Integer _account_id;
   public String name;
   public String email;
+  public String username;
 
   private void fill(Account account, boolean detailed) {
     name = account.getFullName();
     if (detailed) {
       _account_id = account.getId().get();
       email = account.getPreferredEmail();
+      username = account.getUserName();
     }
   }
 }
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..cff3a35 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
@@ -16,6 +16,7 @@
 
 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;
@@ -34,24 +35,28 @@
 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
@@ -116,4 +121,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/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 942b0d7..d2014ec 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
@@ -142,6 +142,12 @@
         || canAdministrateServer();
   }
 
+  /** @return true if the user can stream Gerrit events. */
+  public boolean canStreamEvents() {
+    return canPerform(GlobalCapability.STREAM_EVENTS)
+        || canAdministrateServer();
+  }
+
   /** @return true if the user can run the Git garbage collection. */
   public boolean canRunGC() {
     return canPerform(GlobalCapability.RUN_GC)
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..4a1f4db
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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 String username;
+
+  @Inject
+  CreateAccount(ReviewDb db, IdentifiedUser currentUser,
+      GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
+      AccountCache accountCache, AccountByEmailCache byEmailCache,
+      @Assisted String username) {
+    this.db = db;
+    this.currentUser = currentUser;
+    this.groupsCollection = groupsCollection;
+    this.sshKeyCache = sshKeyCache;
+    this.accountCache = accountCache;
+    this.byEmailCache = byEmailCache;
+    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);
+    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())));
+      db.accountGroupMembers().insert(Collections.singleton(m));
+    }
+
+    sshKeyCache.evict(username);
+    accountCache.evictByUsername(username);
+    byEmailCache.evict(input.email);
+
+    return Response.created(AccountInfo.parse(a, true));
+  }
+
+  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/GetAvatarChangeUrl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
new file mode 100644
index 0000000..ec538bc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.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.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.inject.Inject;
+
+public class GetAvatarChangeUrl implements RestReadView<AccountResource> {
+  private final DynamicItem<AvatarProvider> avatarProvider;
+
+  @Inject
+  GetAvatarChangeUrl(DynamicItem<AvatarProvider> avatarProvider) {
+    this.avatarProvider = avatarProvider;
+  }
+
+  @Override
+  public String apply(AccountResource rsrc)
+      throws ResourceNotFoundException {
+    AvatarProvider impl = avatarProvider.get();
+    if (impl == null) {
+      throw new ResourceNotFoundException();
+    }
+
+    String url = impl.getChangeAvatarUrl(rsrc.getUser());
+    if (Strings.isNullOrEmpty(url)) {
+      throw new ResourceNotFoundException();
+    } else {
+      return 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 81221aa..54f1980 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
@@ -24,12 +24,11 @@
 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;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
 
-import com.google.common.base.Function;
-import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
@@ -40,6 +39,7 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.AccountResource.Capability;
 import com.google.gerrit.server.git.QueueProvider;
@@ -63,14 +63,7 @@
     if (query == null) {
       query = Sets.newHashSet();
     }
-    Iterables.addAll(query, Iterables.transform(
-        Splitter.onPattern("[, ]").omitEmptyStrings().trimResults().split(name),
-        new Function<String, String>() {
-          @Override
-          public String apply(String input) {
-            return input.toLowerCase();
-          }
-        }));
+    Iterables.addAll(query, OptionUtil.splitOptionValue(name));
   }
   private Set<String> query;
 
@@ -112,6 +105,7 @@
     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());
 
     QueueProvider.QueueType queue = cc.getQueueType();
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 b4e770f..34db967 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
 
 import java.util.Collection;
 
@@ -44,7 +45,9 @@
   GroupDescription.Basic get(AccountGroup.UUID uuid);
 
   /** @return suggestions for the group name sorted by name. */
-  Collection<GroupReference> suggest(String name);
+  Collection<GroupReference> suggest(
+      String name,
+      @Nullable ProjectControl project);
 
   /** @return the group membership checker for the backend. */
   GroupMembership membershipsOf(IdentifiedUser user);
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 cdbb0e4..f7e0634 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
@@ -16,6 +16,8 @@
 
 import com.google.common.collect.Iterables;
 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;
@@ -36,7 +38,7 @@
   };
 
   /**
-   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
    * the best suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
@@ -44,9 +46,23 @@
    * @return the best single GroupReference suggestion
    */
   @Nullable
-  public static GroupReference findBestSuggestion(
-      GroupBackend groupBackend, String name) {
-    Collection<GroupReference> refs = groupBackend.suggest(name);
+  public static GroupReference findBestSuggestion(GroupBackend groupBackend,
+      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.
+   *
+   * @param groupBackend the group backend
+   * @param name the name for which to suggest groups
+   * @param project the project for which to suggest groups
+   * @return the best single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findBestSuggestion(GroupBackend groupBackend,
+      String name, @Nullable ProjectControl project) {
+    Collection<GroupReference> refs = groupBackend.suggest(name, project);
     if (refs.size() == 1) {
       return Iterables.getOnlyElement(refs);
     }
@@ -60,7 +76,7 @@
   }
 
   /**
-   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
    * the exact suggestion, or null if one does not exist.
    *
    * @param groupBackend the group backend
@@ -70,7 +86,22 @@
   @Nullable
   public static GroupReference findExactSuggestion(
       GroupBackend groupBackend, String name) {
-    Collection<GroupReference> refs = groupBackend.suggest(name);
+    return findExactSuggestion(groupBackend, name, null);
+  }
+
+  /**
+   * Runs {@link GroupBackend#suggest(String, Project)} 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
+   * @param project the project for which to suggest groups
+   * @return the exact single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findExactSuggestion(
+      GroupBackend groupBackend, String name, ProjectControl project) {
+    Collection<GroupReference> refs = groupBackend.suggest(name, project);
     for (GroupReference ref : refs) {
       if (isExactSuggestion(ref, name)) {
         return ref;
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 5c9f012..2a8e7c9 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -127,6 +128,7 @@
   public boolean isVisible() {
     AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
     return (accountGroup != null && accountGroup.isVisibleToAll())
+      || user instanceof InternalUser
       || user.getEffectiveGroups().contains(group.getGroupUUID())
       || isOwner();
   }
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..2efc611 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
@@ -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();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index d06db4d..a70f942 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -71,7 +72,8 @@
   }
 
   @Override
-  public Collection<GroupReference> suggest(final String name) {
+  public Collection<GroupReference> suggest(final String name,
+      final ProjectControl project) {
     Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
         new Predicate<AccountGroup>() {
           @Override
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 a8bd6e3..dedcc03 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
@@ -19,6 +19,7 @@
 
 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
@@ -29,12 +30,16 @@
     DynamicMap.mapOf(binder(), ACCOUNT_KIND);
     DynamicMap.mapOf(binder(), CAPABILITY_KIND);
 
+    put(ACCOUNT_KIND).to(PutAccount.class);
     get(ACCOUNT_KIND).to(GetAccount.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.diff").to(GetDiffPreferences.class);
     put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
+
+    install(new FactoryModuleBuilder().build(CreateAccount.Factory.class));
   }
 }
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..34e9143
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.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.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 \"" + resource.getUser().getNameEmail()
+        + "\" already exists");
+  }
+}
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 d9c9257..046dfa5 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -82,10 +83,10 @@
   }
 
   @Override
-  public Collection<GroupReference> suggest(String name) {
+  public Collection<GroupReference> suggest(String name, ProjectControl project) {
     Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
     for (GroupBackend g : backends) {
-      groups.addAll(g.suggest(name));
+      groups.addAll(g.suggest(name, project));
     }
     return groups;
   }
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 2cf372b..97a0309 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
@@ -164,7 +165,7 @@
   }
 
   @Override
-  public Collection<GroupReference> suggest(String name) {
+  public Collection<GroupReference> suggest(String name, ProjectControl project) {
     AccountGroup.UUID uuid = new AccountGroup.UUID(name);
     if (isLdapUUID(uuid)) {
       GroupDescription.Basic g = get(uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index fc1102e..ac47cb5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -239,7 +239,7 @@
         }
       }
     } catch (NamingException e) {
-      log.error("Cannot query LDAP to autenticate user", e);
+      log.error("Cannot query LDAP to authenticate user", e);
       throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     } catch (LoginException e) {
       log.error("Cannot authenticate server via JAAS", e);
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 e0c78ea..5c965e2 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
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -29,11 +30,9 @@
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.util.Collections;
-import java.util.List;
 import java.util.Set;
 
 public class ChangeInserter {
@@ -53,17 +52,27 @@
   }
 
   public void insertChange(ReviewDb db, Change change, PatchSet ps,
-      RevCommit commit, LabelTypes labelTypes, List<FooterLine> footerLines,
-      PatchSetInfo info, Set<Account.Id> reviewers) throws OrmException {
+      RevCommit commit, LabelTypes labelTypes, PatchSetInfo info,
+      Set<Account.Id> reviewers) throws OrmException {
+    insertChange(db, change, null, ps, commit, labelTypes, info, reviewers);
+  }
+
+  public void insertChange(ReviewDb db, Change change,
+      ChangeMessage changeMessage, PatchSet ps, RevCommit commit,
+      LabelTypes labelTypes, PatchSetInfo info, Set<Account.Id> reviewers)
+      throws OrmException {
 
     db.changes().beginTransaction(change.getId());
     try {
       ChangeUtil.insertAncestors(db, ps.getId(), commit);
       db.patchSets().insert(Collections.singleton(ps));
       db.changes().insert(Collections.singleton(change));
-      ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+      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));
+      }
       db.commit();
     } finally {
       db.rollback();
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 f6361a9..563ab81 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
@@ -64,9 +64,6 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 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;
@@ -124,7 +121,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeControl.GenericFactory changeControlGenericFactory;
   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;
@@ -143,7 +140,7 @@
       IdentifiedUser.GenericFactory uf,
       ChangeControl.GenericFactory ccf,
       PatchSetInfoFactory psi,
-      PatchListCache plc,
+      FileInfoJson fileInfoJson,
       AccountInfo.Loader.Factory ailf,
       @CanonicalWebUrl Provider<String> curl,
       Urls urls) {
@@ -154,7 +151,7 @@
     this.userFactory = uf;
     this.changeControlGenericFactory = ccf;
     this.patchSetInfoFactory = psi;
-    this.patchListCache = plc;
+    this.fileInfoJson = fileInfoJson;
     this.accountLoaderFactory = ailf;
     this.urlProvider = curl;
     this.urls = urls;
@@ -207,12 +204,15 @@
 
   public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in)
       throws OrmException {
-    accountLoader =
-        accountLoaderFactory.create(options.contains(DETAILED_ACCOUNTS));
+    accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
     for (List<ChangeData> changes : in) {
       ChangeData.ensureChangeLoaded(db, changes);
-      ChangeData.ensureCurrentPatchSetLoaded(db, changes);
+      if (has(ALL_REVISIONS)) {
+        ChangeData.ensureAllPatchSetsLoaded(db, changes);
+      } else {
+        ChangeData.ensureCurrentPatchSetLoaded(db, changes);
+      }
       ChangeData.ensureCurrentApprovalsLoaded(db, changes);
       res.add(toChangeInfo(changes));
     }
@@ -220,6 +220,10 @@
     return res;
   }
 
+  private boolean has(ListChangesOption option) {
+    return options.contains(option);
+  }
+
   private List<ChangeInfo> toChangeInfo(List<ChangeData> changes)
       throws OrmException {
     List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
@@ -246,10 +250,15 @@
     out._sortkey = in.getSortKey();
     out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
     out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
-    out.labels = labelsFor(cd, options.contains(LABELS),
-        options.contains(DETAILED_LABELS));
-    if (out.labels != null && options.contains(DETAILED_LABELS)) {
-      out.permitted_labels = permittedLabels(cd);
+    out.labels = labelsFor(cd, has(LABELS), has(DETAILED_LABELS));
+
+    Collection<PatchSet.Id> limited = cd.getLimitedPatchSets();
+    if (out.labels != null && has(DETAILED_LABELS)) {
+      // If limited to specific patch sets but not the current patch set, don't
+      // list permitted labels, since users can't vote on those patch sets.
+      if (limited == null || limited.contains(in.currentPatchSetId())) {
+        out.permitted_labels = permittedLabels(cd);
+      }
       out.removable_reviewers = removableReviewers(cd, out.labels.values());
     }
     if (options.contains(MESSAGES)) {
@@ -257,8 +266,7 @@
     }
     out.finish();
 
-    if (options.contains(ALL_REVISIONS) || options.contains(CURRENT_REVISION)
-        || cd.getLimitedPatchSets() != null) {
+    if (has(ALL_REVISIONS) || has(CURRENT_REVISION) || limited != null) {
       out.revisions = revisions(cd);
       if (out.revisions != null) {
         for (String commit : out.revisions.keySet()) {
@@ -462,16 +470,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));
       }
     }
   }
@@ -515,7 +525,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);
         }
@@ -530,6 +540,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());
@@ -554,9 +565,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;
   }
@@ -719,7 +731,7 @@
     }
 
     Collection<PatchSet> src;
-    if (cd.getLimitedPatchSets() != null || options.contains(ALL_REVISIONS)) {
+    if (cd.getLimitedPatchSets() != null || has(ALL_REVISIONS)) {
       src = cd.patches(db);
     } else {
       src = Collections.singletonList(cd.currentPatchSet(db));
@@ -741,8 +753,7 @@
     out.draft = in.isDraft() ? true : null;
     out.fetch = makeFetchMap(cd, in);
 
-    if (options.contains(ALL_COMMITS)
-        || (out.isCurrent && options.contains(CURRENT_COMMIT))) {
+    if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
       try {
         PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
         out.commit = new CommitInfo();
@@ -763,51 +774,12 @@
       }
     }
 
-    if (options.contains(ALL_FILES)
-        || (out.isCurrent && options.contains(CURRENT_FILES))) {
-      PatchList list;
+    if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
       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;
-            }
-          }
-        }
       }
     }
     return out;
@@ -897,7 +869,7 @@
     int _number;
     Map<String, FetchInfo> fetch;
     CommitInfo commit;
-    Map<String, FileInfo> files;
+    Map<String, FileInfoJson.FileInfo> files;
   }
 
   static class FetchInfo {
@@ -926,14 +898,6 @@
     String message;
   }
 
-  static class FileInfo {
-    Character status;
-    Boolean binary;
-    String oldPath;
-    Integer linesInserted;
-    Integer linesDeleted;
-  }
-
   static class LabelInfo {
     transient SubmitRecord.Label.Status _status;
 
@@ -958,6 +922,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/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
index 3c1c07f..79c442c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -62,6 +62,8 @@
   }
 
   public static class ParseException extends Exception {
+    private static final long serialVersionUID = 1L;
+
     ParseException() {
       super();
     }
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..3fb85b4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -0,0 +1,93 @@
+// 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.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.change.CherryPickChange;
+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> {
+  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());
+    }
+  }
+}
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..d4fc61b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -0,0 +1,269 @@
+// 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.data.LabelTypes;
+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.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.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.patch.PatchSetInfoFactory;
+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.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.sql.Timestamp;
+import java.util.Collections;
+import java.util.List;
+
+public class CherryPickChange {
+
+  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ReviewDb db;
+  private final GitRepositoryManager gitManager;
+  private final PersonIdent myIdent;
+  private final IdentifiedUser currentUser;
+  private final CommitValidators.Factory commitValidatorsFactory;
+  private final ChangeInserter changeInserter;
+  private final PatchSetInserter patchSetInserter;
+  final MergeUtil.Factory mergeUtilFactory;
+
+  @Inject
+  CherryPickChange(final PatchSetInfoFactory patchSetInfoFactory,
+      final ReviewDb db, @GerritPersonIdent final PersonIdent myIdent,
+      final GitRepositoryManager gitManager, final IdentifiedUser currentUser,
+      final CommitValidators.Factory commitValidatorsFactory,
+      final ChangeInserter changeInserter,
+      final PatchSetInserter patchSetInserter,
+      final MergeUtil.Factory mergeUtilFactory) {
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.db = db;
+    this.gitManager = gitManager;
+    this.myIdent = myIdent;
+    this.currentUser = currentUser;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.changeInserter = changeInserter;
+    this.patchSetInserter = patchSetInserter;
+    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 {
+      CommitValidators commitValidators =
+          commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
+
+      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());
+
+        RevCommit cherryPickCommit;
+        ObjectInserter oi = git.newObjectInserter();
+        try {
+          ProjectState projectState = refControl.getProjectControl().getProjectState();
+          cherryPickCommit =
+              mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
+                  commitToCherryPick, committerIdent, message, 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 {
+          final ObjectId computedChangeId =
+              ChangeIdUtil
+                  .computeChangeId(cherryPickCommit.getTree(), mergeTip,
+                      cherryPickCommit.getAuthorIdent(), myIdent, message);
+
+          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();
+
+        Change change;
+        boolean createNewChange;
+        PatchSet.Id id;
+        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.
+          change = destChanges.get(0);
+          id = ChangeUtil.nextPatchSetId(git, change.currentPatchSetId());
+
+          createNewChange = false;
+        } else {
+          // Change key not found on destination branch. We can create a new
+          // change.
+          change =
+              new Change(changeKey, new Change.Id(db.nextChangeId()),
+                  currentUser.getAccountId(), new Branch.NameKey(project,
+                      destRef.getName()));
+          id = new PatchSet.Id(change.getId(), Change.INITIAL_PATCH_SET_ID);
+          createNewChange = true;
+        }
+        PatchSet newPatchSet = new PatchSet(id);
+        newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+        newPatchSet.setUploader(change.getOwner());
+        newPatchSet.setRevision(new RevId(cherryPickCommit.name()));
+
+        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()));
+        }
+
+        final ChangeMessage cmsg =
+            new ChangeMessage(new ChangeMessage.Key(changeId,
+                ChangeUtil.messageUUID(db)), currentUser.getAccountId(),
+                patchSetId);
+        final StringBuilder msgBuf =
+            new StringBuilder("Patch Set " + patchSetId.get()
+                + ": Cherry Picked");
+        msgBuf.append("\n\n");
+        msgBuf.append("This patchset was cherry picked to change: "
+            + change.getKey().get());
+        cmsg.setMessage(msgBuf.toString());
+
+        if (createNewChange) {
+          LabelTypes labelTypes =
+              refControl.getProjectControl().getLabelTypes();
+
+          PatchSetInfo newPatchSetInfo =
+              patchSetInfoFactory.get(cherryPickCommit, newPatchSet.getId());
+          change.setCurrentPatchSet(newPatchSetInfo);
+          ChangeUtil.updated(change);
+
+          changeInserter.insertChange(db, change, cmsg, newPatchSet,
+              cherryPickCommit, labelTypes, newPatchSetInfo,
+              Collections.<Account.Id> emptySet());
+        } else {
+          patchSetInserter.insertPatchSet(change, newPatchSet,
+              cherryPickCommit, refControl, cmsg, false);
+        }
+
+        return change.getId();
+      } finally {
+        revWalk.release();
+      }
+    } finally {
+      git.close();
+    }
+  }
+}
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..fec72d6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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;
+
+import javax.annotation.Nullable;
+
+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/PatchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
similarity index 80%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
index d3cf5c6..2efbf72 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
@@ -20,14 +20,14 @@
 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>>() {};
+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;
 
-  PatchResource(RevisionResource rev, String name) {
+  FileResource(RevisionResource rev, String name) {
     this.rev = rev;
     this.key = new Patch.Key(rev.getPatchSet().getId(), name);
   }
@@ -39,4 +39,8 @@
   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..54f4f29
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.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.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.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchSet;
+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.kohsuke.args4j.Option;
+
+class Files implements ChildCollection<RevisionResource, FileResource> {
+  private final DynamicMap<RestView<FileResource>> views;
+  private final FileInfoJson fileInfoJson;
+  private final Provider<Revisions> revisions;
+
+  @Inject
+  Files(DynamicMap<RestView<FileResource>> views,
+      FileInfoJson fileInfoJson,
+      Provider<Revisions> revisions) {
+    this.views = views;
+    this.fileInfoJson = fileInfoJson;
+    this.revisions = revisions;
+  }
+
+  @Override
+  public DynamicMap<RestView<FileResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<RevisionResource> list() throws AuthException {
+    return new List();
+  }
+
+  @Override
+  public FileResource parse(RevisionResource rev, IdString id)
+      throws ResourceNotFoundException, OrmException, AuthException {
+    return new FileResource(rev, id.get());
+  }
+
+  private final class List implements RestReadView<RevisionResource> {
+    @Option(name = "--base", metaVar = "revision-id")
+    String base;
+
+    @Override
+    public Object apply(RevisionResource resource)
+        throws ResourceNotFoundException, OrmException,
+        PatchListNotAvailableException {
+      PatchSet basePatchSet = null;
+      if (base != null) {
+        RevisionResource baseResource = revisions.get().parse(
+            resource.getChangeResource(), IdString.fromDecoded(base));
+        basePatchSet = baseResource.getPatchSet();
+      }
+      return fileInfoJson.toFileInfoMap(
+          resource.getChange(), resource.getPatchSet(), basePatchSet);
+    }
+  }
+}
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..7c8ec34
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
new file mode 100644
index 0000000..a06831a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -0,0 +1,135 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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 java.io.IOException;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+
+public class GetPatch implements RestReadView<RevisionResource> {
+  private final GitRepositoryManager repoManager;
+
+  @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 {
+              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();
+            }
+          }.setContentType("application/mbox")
+           .base64();
+          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());
+  }
+}
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..e088e4b 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;
 
@@ -35,12 +35,12 @@
     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);
 
@@ -53,6 +53,7 @@
     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, "reviewers").to(PostReviewers.class);
     child(CHANGE_KIND, "reviewers").to(Reviewers.class);
@@ -63,10 +64,14 @@
     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);
     get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
+    get(REVISION_KIND, "patch").to(GetPatch.class);
     post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
     post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
 
+    post(REVISION_KIND, "cherrypick").to(CherryPick.class);
+
     child(REVISION_KIND, "drafts").to(Drafts.class);
     put(REVISION_KIND, "drafts").to(CreateDraft.class);
     get(DRAFT_KIND).to(GetDraft.class);
@@ -76,9 +81,10 @@
     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);
 
     install(new FactoryModule() {
       @Override
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..1c25a7c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.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.change;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.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.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.util.Collections;
+import java.util.List;
+
+public class PatchSetInserter {
+  private final ChangeHooks hooks;
+  private final TrackingFooters trackingFooters;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ReviewDb db;
+  private final IdentifiedUser user;
+
+  @Inject
+  public PatchSetInserter(final ChangeHooks hooks,
+      final TrackingFooters trackingFooters, final ReviewDb db,
+      final PatchSetInfoFactory patchSetInfoFactory, final IdentifiedUser user) {
+    this.hooks = hooks;
+    this.trackingFooters = trackingFooters;
+    this.db = db;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.user = user;
+  }
+
+  public Change insertPatchSet(Change change, final PatchSet patchSet,
+      final RevCommit commit, RefControl refControl, String message,
+      boolean copyLabels) throws OrmException, InvalidChangeOperationException,
+      NoSuchChangeException {
+    final ChangeMessage cmsg =
+        new ChangeMessage(new ChangeMessage.Key(change.getId(),
+            ChangeUtil.messageUUID(db)), user.getAccountId(), patchSet.getId());
+    cmsg.setMessage(message);
+
+    return insertPatchSet(change, patchSet, commit, refControl, cmsg,
+        copyLabels);
+  }
+
+  public Change insertPatchSet(Change change, final PatchSet patchSet,
+      final RevCommit commit, RefControl refControl, ChangeMessage changeMessage,
+      boolean copyLabels) throws OrmException, InvalidChangeOperationException,
+      NoSuchChangeException {
+
+    final PatchSet.Id currentPatchSetId = change.currentPatchSetId();
+
+    if (patchSet.getId().get() <= currentPatchSetId.get()) {
+      throw new InvalidChangeOperationException("New Patch Set ID ["
+          + patchSet.getId().get()
+          + "] is not greater than the current Patch Set ID ["
+          + currentPatchSetId.get() + "]");
+    }
+
+    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));
+
+      Change 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) {
+        change = updatedChange;
+      } else {
+        throw new ChangeModifiedException(String.format(
+            "Change %s was modified", change.getId()));
+      }
+
+      if (copyLabels) {
+        ApprovalsUtil.copyLabels(db, refControl.getProjectControl()
+            .getLabelTypes(), currentPatchSetId, change.currentPatchSetId());
+      }
+
+      final List<FooterLine> footerLines = commit.getFooterLines();
+      ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+
+      if (changeMessage != null) {
+        db.changeMessages().insert(Collections.singleton(changeMessage));
+      }
+      db.commit();
+
+      hooks.doPatchsetCreatedHook(change, patchSet, db);
+    } finally {
+      db.rollback();
+    }
+    return change;
+  }
+
+  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 044e266..9c14bcb 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
@@ -132,6 +132,7 @@
       checkComments(input.comments);
     }
     if (input.notify == null) {
+      log.warn("notify = null; assuming notify = NONE");
       input.notify = NotifyHandling.NONE;
     }
 
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..59458e4 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
@@ -56,6 +56,7 @@
 
 import org.eclipse.jgit.lib.Config;
 
+import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.List;
 import java.util.Set;
@@ -120,7 +121,7 @@
   @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");
     }
@@ -147,8 +148,8 @@
   }
 
   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())) {
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..358e5fc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.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.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.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> {
+  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());
+  }
+
+  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/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index c268530..154bd64 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
@@ -27,8 +27,6 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.Revert.Input;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.RevertedSender;
@@ -42,8 +40,6 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-import javax.annotation.Nullable;
-
 public class Revert implements RestModifyView<ChangeResource, Input> {
   private final ChangeHooks hooks;
   private final RevertedSender.Factory revertedSenderFactory;
@@ -53,8 +49,7 @@
   private final GitRepositoryManager gitManager;
   private final PersonIdent myIdent;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final String canonicalWebUrl;
+  private final ChangeInserter changeInserter;
 
   public static class Input {
     public String message;
@@ -68,9 +63,8 @@
       ChangeJson json,
       GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated gitRefUpdated,
       @GerritPersonIdent final PersonIdent myIdent,
-      @CanonicalWebUrl @Nullable final String canonicalWebUrl) {
+      final ChangeInserter changeInserter) {
     this.hooks = hooks;
     this.revertedSenderFactory = revertedSenderFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
@@ -78,9 +72,8 @@
     this.json = json;
     this.gitManager = gitManager;
     this.myIdent = myIdent;
-    this.gitRefUpdated = gitRefUpdated;
+    this.changeInserter = changeInserter;
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.canonicalWebUrl = canonicalWebUrl;
   }
 
   @Override
@@ -104,7 +97,7 @@
               commitValidators,
               Strings.emptyToNull(input.message), dbProvider.get(),
               revertedSenderFactory, hooks, git, patchSetInfoFactory,
-              gitRefUpdated, myIdent, canonicalWebUrl);
+              myIdent, changeInserter);
 
       return json.format(revertedChangeId);
     } catch (InvalidChangeOperationException e) {
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..42d81b6 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
@@ -28,7 +28,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,7 +37,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);
@@ -52,7 +52,7 @@
     }
   }
 
-  static class DeleteReviewed implements RestModifyView<PatchResource, Input> {
+  static class DeleteReviewed implements RestModifyView<FileResource, Input> {
     private final Provider<ReviewDb> dbProvider;
 
     @Inject
@@ -61,7 +61,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 +73,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..93e991e 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
@@ -35,8 +35,12 @@
     this.ps = ps;
   }
 
+  public ChangeResource getChangeResource() {
+    return change;
+  }
+
   public ChangeControl getControl() {
-    return change.getControl();
+    return getChangeResource().getControl();
   }
 
   public Change getChange() {
@@ -48,6 +52,10 @@
   }
 
   Account.Id getAccountId() {
-    return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
+    return getUser().getAccountId();
+  }
+
+  IdentifiedUser getUser() {
+    return (IdentifiedUser) getControl().getCurrentUser();
   }
 }
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 c63bf5d..2b51b0a 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
@@ -106,6 +106,10 @@
 
     checkSubmitRule(rsrc);
     change = submit(rsrc, caller);
+    if (change == null) {
+      throw new ResourceConflictException("change is "
+          + status(dbProvider.get().changes().get(rsrc.getChange().getId())));
+    }
 
     if (input.waitForMerge) {
       mergeQueue.merge(change.getDest());
@@ -123,21 +127,7 @@
       case MERGED:
         return new Output(Status.MERGED, change);
       case NEW:
-        // 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 message and report that as the conflict.
-        final Timestamp before = rsrc.getChange().getLastUpdatedOn();
-        ChangeMessage msg = Iterables.getFirst(Iterables.filter(
-          Lists.reverse(dbProvider.get().changeMessages()
-              .byChange(change.getId())
-              .toList()),
-          new Predicate<ChangeMessage>() {
-            @Override
-            public boolean apply(ChangeMessage input) {
-              return input.getAuthor() == null
-                  && input.getWrittenOn().getTime() >= before.getTime();
-            }
-          }), null);
+        ChangeMessage msg = getConflictMessage(rsrc);
         if (msg != null) {
           throw new ResourceConflictException(msg.getMessage());
         }
@@ -146,8 +136,30 @@
     }
   }
 
-  private Change submit(RevisionResource rsrc, IdentifiedUser caller)
-      throws OrmException, ResourceConflictException {
+  /**
+   * 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
+   * message and return it.
+   */
+  public ChangeMessage getConflictMessage(RevisionResource rsrc)
+      throws OrmException {
+    final Timestamp before = rsrc.getChange().getLastUpdatedOn();
+    ChangeMessage msg = Iterables.getFirst(Iterables.filter(
+      Lists.reverse(dbProvider.get().changeMessages()
+          .byChange(rsrc.getChange().getId())
+          .toList()),
+      new Predicate<ChangeMessage>() {
+        @Override
+        public boolean apply(ChangeMessage input) {
+          return input.getAuthor() == null
+              && input.getWrittenOn().getTime() >= before.getTime();
+        }
+      }), null);
+    return msg;
+  }
+
+  public Change submit(RevisionResource rsrc, IdentifiedUser caller)
+      throws OrmException {
     final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
     Change change = rsrc.getChange();
     ReviewDb db = dbProvider.get();
@@ -169,8 +181,7 @@
           }
         });
       if (change == null) {
-        throw new ResourceConflictException("change is "
-            + status(db.changes().get(rsrc.getChange().getId())));
+        return null;
       }
       db.commit();
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index 170f8e8..db18f0d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -129,10 +129,9 @@
     return out;
   }
 
-  @SuppressWarnings("unchecked")
   private static List<Term> eval(SubmitRuleEvaluator evaluator)
       throws RuleEvalException {
-    return evaluator.evaluate().toJava();
+    return evaluator.evaluate();
   }
 
   static class Record {
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 f58fe75..e7e1f32 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
@@ -30,12 +30,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
 import org.kohsuke.args4j.Option;
 
 import java.io.ByteArrayInputStream;
+import java.util.List;
 
 public class TestSubmitType implements RestModifyView<RevisionResource, Input> {
   private final ReviewDb db;
@@ -76,7 +76,7 @@
           ? new ByteArrayInputStream(input.rule.getBytes(Charsets.UTF_8))
           : null);
 
-    ListTerm results;
+    List<Term> results;
     try {
       results = evaluator.evaluate();
     } catch (RuleEvalException e) {
@@ -84,12 +84,12 @@
           "rule failed with exception: %s",
           e.getMessage()));
     }
-    if (results.isNil()) {
+    if (results.isEmpty()) {
       throw new BadRequestException(String.format(
           "rule %s has no solution",
           evaluator.getSubmitRule()));
     }
-    Term type = results.car();
+    Term type = results.get(0);
     if (!type.isSymbol()) {
       throw new BadRequestException(String.format(
           "rule %s produced invalid result: %s",
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 e5f6b8f..37cbdf7 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
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
+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.git.MergeUtil;
@@ -40,6 +41,7 @@
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -63,7 +65,7 @@
 import java.util.Set;
 
 public class RebaseChange {
-  private final ChangeControl.Factory changeControlFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
@@ -72,16 +74,20 @@
   private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
   private final ChangeHookRunner hooks;
   private final MergeUtil.Factory mergeUtilFactory;
+  private final ProjectCache projectCache;
+  private final IdentifiedUser currentUser;
 
   @Inject
-  RebaseChange(final ChangeControl.Factory changeControlFactory,
+  RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
       final PatchSetInfoFactory patchSetInfoFactory, 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 MergeUtil.Factory mergeUtilFactory,
+      final ProjectCache projectCache,
+      final IdentifiedUser currentUser) {
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.db = db;
@@ -91,6 +97,8 @@
     this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
     this.hooks = hooks;
     this.mergeUtilFactory = mergeUtilFactory;
+    this.projectCache = projectCache;
+    this.currentUser = currentUser;
   }
 
   /**
@@ -119,12 +127,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: "
@@ -147,10 +155,14 @@
       final RevCommit baseCommit =
           rw.parseCommit(ObjectId.fromString(baseRev));
 
+      PersonIdent committerIdent =
+          currentUser.newCommitterIdent(myIdent.getWhen(),
+              myIdent.getTimeZone());
+
       final PatchSet newPatchSet =
-          rebase(git, rw, inserter, patchSetId, change, uploader, baseCommit,
+          rebase(git, rw, inserter, patchSetId, change, uploader.getAccountId(), baseCommit,
               mergeUtilFactory.create(
-                  changeControl.getProjectControl().getProjectState(), true));
+                  changeControl.getProjectControl().getProjectState(), true), committerIdent);
 
       final Set<Account.Id> oldReviewers = Sets.newHashSet();
       final Set<Account.Id> oldCC = Sets.newHashSet();
@@ -163,7 +175,7 @@
       }
       final ReplacePatchSetSender cm =
           rebasedPatchSetSenderFactory.create(change);
-      cm.setFrom(uploader);
+      cm.setFrom(uploader.getAccountId());
       cm.setPatchSet(newPatchSet);
       cm.addReviewers(oldReviewers);
       cm.addExtraCC(oldCC);
@@ -302,7 +314,7 @@
   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 MergeUtil mergeUtil, PersonIdent committerIdent) throws NoSuchChangeException,
       OrmException, IOException, InvalidChangeOperationException,
       PathConflictException {
     Change change = chg;
@@ -311,7 +323,7 @@
     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);
 
@@ -374,10 +386,8 @@
             "Change %s was modified", change.getId()));
       }
 
-      ApprovalsUtil.copyLabels(db,
-          changeControlFactory.controlFor(change).getLabelTypes(),
-          patchSetId,
-          change.currentPatchSetId());
+      ApprovalsUtil.copyLabels(db, projectCache.get(change.getProject())
+          .getLabelTypes(), patchSetId, change.currentPatchSetId());
 
       final ChangeMessage cmsg =
           new ChangeMessage(new ChangeMessage.Key(change.getId(),
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 0c68c49..62c6863 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
@@ -28,10 +28,12 @@
 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.FileTypeRegistry;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
@@ -63,6 +65,7 @@
 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;
@@ -92,6 +95,8 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CommentLinkInfo;
+import com.google.gerrit.server.project.CommentLinkProvider;
 import com.google.gerrit.server.project.PerformCreateProject;
 import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
@@ -111,6 +116,8 @@
 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 {
@@ -181,6 +188,7 @@
     factory(MergeUtil.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);
@@ -209,6 +217,8 @@
     bind(EventFactory.class);
     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);
@@ -249,5 +259,8 @@
     bind(AccountManager.class);
     bind(ChangeUserName.CurrentUser.class);
     factory(ChangeUserName.Factory.class);
+
+    bind(new TypeLiteral<List<CommentLinkInfo>>() {})
+        .toProvider(CommentLinkProvider.class).in(SINGLETON);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index ec14883..af14015 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -16,10 +16,8 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
 import com.google.gerrit.server.changedetail.PublishDraft;
 import com.google.gerrit.server.git.BanCommit;
@@ -39,8 +37,6 @@
     bind(RequestCleanup.class).in(RequestScoped.class);
     bind(RequestScopedReviewDbProvider.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
-    bind(ApprovalsUtil.class);
-    bind(ChangeInserter.class);
 
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ChangeControl.Factory.class).in(SINGLETON);
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 8d76e90..2116c0c 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
@@ -24,6 +24,10 @@
 /** Important paths within a {@link SitePath}. */
 @Singleton
 public final class SitePaths {
+  public static final String CSS_FILENAME = "GerritSite.css";
+  public static final String HEADER_FILENAME = "GerritSiteHeader.html";
+  public static final String FOOTER_FILENAME = "GerritSiteFooter.html";
+
   public final File site_path;
   public final File bin_dir;
   public final File etc_dir;
@@ -35,6 +39,7 @@
   public final File mail_dir;
   public final File hooks_dir;
   public final File static_dir;
+  public final File themes_dir;
 
   public final File gerrit_sh;
   public final File gerrit_war;
@@ -71,6 +76,7 @@
     mail_dir = new File(etc_dir, "mail");
     hooks_dir = new File(site_path, "hooks");
     static_dir = new File(site_path, "static");
+    themes_dir = new File(site_path, "themes");
 
     gerrit_sh = new File(bin_dir, "gerrit.sh");
     gerrit_war = new File(bin_dir, "gerrit.war");
@@ -85,9 +91,9 @@
     ssh_dsa = new File(etc_dir, "ssh_host_dsa_key");
     peer_keys = new File(etc_dir, "peer_keys");
 
-    site_css = new File(etc_dir, "GerritSite.css");
-    site_header = new File(etc_dir, "GerritSiteHeader.html");
-    site_footer = new File(etc_dir, "GerritSiteFooter.html");
+    site_css = new File(etc_dir, CSS_FILENAME);
+    site_header = new File(etc_dir, HEADER_FILENAME);
+    site_footer = new File(etc_dir, FOOTER_FILENAME);
     site_gitweb = new File(etc_dir, "gitweb_config.perl");
 
     if (site_path.exists()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java
index 2d88b83..e5627c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class AccountAttribute {
     public String name;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java
index baa660c..3059be3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class ApprovalAttribute {
     public String type;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
similarity index 96%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
index 5150b48..7339829 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import com.google.gerrit.reviewdb.client.Change;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
index 47fbdac..4c796f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class DependencyAttribute {
   public String id;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
index 71b38b5..f18beba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class MessageAttribute {
     public Long timestamp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
index 82f44a1..12ac30a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
similarity index 96%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index 1123e5f..79d82e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import java.util.List;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
index e0c8c13..7610068 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class PatchSetCommentAttribute {
     public String file;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
similarity index 90%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
index ecf2b9a..9897065 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
-public class QueryStats {
+public class QueryStatsAttribute {
   public final String type = "stats";
   public int rowCount;
   public long runTimeMilliseconds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
index e4d715a..b3808d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class RefUpdateAttribute {
   public String oldRev;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
index 99d0350..4c774c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class SubmitLabelAttribute {
     public String label;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
index 04b76e1..1ce2ce6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 import java.util.List;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java
index 7d55dd2..473ea43 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
 
 public class TrackingIdAttribute {
   public String system;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
index baaf30c..b0eb9c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ChangeAbandonedEvent extends ChangeEvent {
     public final String type = "change-abandoned";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
index 0d5fc31..38996a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ChangeMergedEvent extends ChangeEvent {
     public final String type = "change-merged";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index 717e23c..e761190 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ChangeRestoredEvent extends ChangeEvent {
     public final String type = "change-restored";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
index f00caaf..52d7409 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class CommentAddedEvent extends ChangeEvent {
     public final String type = "comment-added";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
index c90ac90..7fd033a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class DraftPublishedEvent extends ChangeEvent {
     public final String type = "draft-published";
     public ChangeAttribute change;
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 d556e73..63bfa71 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
@@ -33,6 +33,18 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.DependencyAttribute;
+import com.google.gerrit.server.data.MessageAttribute;
+import com.google.gerrit.server.data.PatchAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.PatchSetCommentAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+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.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
index e6ff525..599fe60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class MergeFailedEvent extends ChangeEvent {
     public final String type = "merge-failed";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
index 15e3978..fbaf4ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class PatchSetCreatedEvent extends ChangeEvent {
     public final String type = "patchset-created";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
index f90bc81..944c9ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+
 public class RefUpdatedEvent extends ChangeEvent {
   public final String type = "ref-updated";
   public AccountAttribute submitter;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index a881d8d..e00cc60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
 public class ReviewerAddedEvent extends ChangeEvent {
     public final String type = "reviewer-added";
     public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiCommands.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiCommands.java
new file mode 100644
index 0000000..0c9626b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiCommands.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.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.common.collect.Sets;
+import com.google.gerrit.common.data.UiCommandDetail;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.webui.UiCommand;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+public class UiCommands {
+  private static final Logger log = LoggerFactory.getLogger(UiCommands.class);
+
+  public static Predicate<UiCommandDetail> enabled() {
+    return new Predicate<UiCommandDetail>() {
+      @Override
+      public boolean apply(UiCommandDetail input) {
+        return input.enabled;
+      }
+    };
+  }
+
+  public static List<UiCommandDetail> sorted(Iterable<UiCommandDetail> in) {
+    List<UiCommandDetail> s = Lists.newArrayList(in);
+    Collections.sort(s, new Comparator<UiCommandDetail>() {
+      @Override
+      public int compare(UiCommandDetail a, UiCommandDetail b) {
+        return a.id.compareTo(b.id);
+      }
+    });
+    return s;
+  }
+
+  public static <R extends RestResource> Iterable<UiCommandDetail> from(
+      ChildCollection<?, R> collection,
+      R resource,
+      EnumSet<UiCommand.Place> places) {
+    return from(collection.views(), resource, places);
+  }
+
+  public static <R extends RestResource> Iterable<UiCommandDetail> from(
+      DynamicMap<RestView<R>> views,
+      final R resource,
+      final EnumSet<UiCommand.Place> places) {
+    return Iterables.filter(
+      Iterables.transform(
+        views,
+        new Function<DynamicMap.Entry<RestView<R>>, UiCommandDetail> () {
+          @Override
+          @Nullable
+          public UiCommandDetail apply(DynamicMap.Entry<RestView<R>> e) {
+            int d = e.getExportName().indexOf('.');
+            if (d < 0) {
+              return null;
+            }
+
+            String method = e.getExportName().substring(0, d);
+            String name = e.getExportName().substring(d + 1);
+            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 UiCommand)) {
+              return null;
+            }
+
+            UiCommand<R> cmd = (UiCommand<R>) view;
+            if (Sets.intersection(cmd.getPlaces(), places).isEmpty()
+                || !cmd.isVisible(resource)) {
+              return null;
+            }
+
+            UiCommandDetail dsc = new UiCommandDetail();
+            dsc.id = e.getPluginName() + '~' + name;
+            dsc.method = method;
+            dsc.label = cmd.getLabel(resource);
+            dsc.title = cmd.getTitle(resource);
+            dsc.enabled = cmd.isEnabled(resource);
+            return dsc;
+          }
+        }),
+      Predicates.notNull());
+  }
+
+  private UiCommands() {
+  }
+}
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..f4ed7cd 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
@@ -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;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 69dcb15..8595472 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-enum CommitMergeStatus {
+public enum CommitMergeStatus {
   /** */
   CLEAN_MERGE("Change has been successfully merged into the git repository."),
 
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 02caa19..a59a216 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
@@ -931,30 +931,29 @@
 
   private PatchSetApproval saveApprovals(Change c, PatchSet.Id merged)
       throws OrmException {
-    // Flatten out all existing approvals based upon the current
-    // permissions. Once the change is closed the approvals are
-    // not updated at presentation view time, so we need to make.
-    // sure they are accurate now. This way if permissions get
-    // modified in the future, historical records stay accurate.
-    //
-    Change.Id changeId = c.getId();
+    // Flatten out existing approvals for this patch set based upon the current
+    // permissions. Once the change is closed the approvals are not updated at
+    // presentation view time, except for zero votes used to indicate a reviewer
+    // was added. So we need to make sure votes are accurate now. This way if
+    // permissions get modified in the future, historical records stay accurate.
     PatchSetApproval submitter = null;
     try {
       c.setStatus(Change.Status.MERGED);
 
       List<PatchSetApproval> approvals =
-          db.patchSetApprovals().byChange(changeId).toList();
+          db.patchSetApprovals().byPatchSet(merged).toList();
       Set<PatchSetApproval.Key> toDelete =
           Sets.newHashSetWithExpectedSize(approvals.size());
       for (PatchSetApproval a : approvals) {
-        toDelete.add(a.getKey());
+        if (a.getValue() != 0) {
+          toDelete.add(a.getKey());
+        }
       }
 
       approvals = labelNormalizer.normalize(c, approvals);
       for (PatchSetApproval a : approvals) {
         toDelete.remove(a.getKey());
-        if (a.getValue() > 0 && a.isSubmit()
-            && a.getPatchSetId().equals(merged)) {
+        if (a.getValue() > 0 && a.isSubmit()) {
           if (submitter == null
               || a.getGranted().compareTo(submitter.getGranted()) > 0) {
             submitter = a;
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 c47084a..3ca5848 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
@@ -180,8 +180,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 {
 
@@ -199,10 +199,8 @@
       mergeCommit.setMessage(commitMsg);
 
       final ObjectId id = commit(inserter, mergeCommit);
-      final CodeReviewCommit newCommit =
-          (CodeReviewCommit) rw.parseCommit(id);
 
-      return newCommit;
+      return rw.parseCommit(id);
     } else {
       return null;
     }
@@ -315,11 +313,11 @@
   }
 
   private static boolean isCodeReview(LabelId id) {
-    return "CRVW".equals(id.get()) || "Code-Review".equalsIgnoreCase(id.get());
+    return "Code-Review".equalsIgnoreCase(id.get());
   }
 
   private static boolean isVerified(LabelId id) {
-    return "VRIF".equals(id.get()) || "Verified".equalsIgnoreCase(id.get());
+    return "Verified".equalsIgnoreCase(id.get());
   }
 
   public List<PatchSetApproval> getApprovalsForCommit(final CodeReviewCommit n) {
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 852b5e7..db70b29 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
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.common.data.Permission.isPermission;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
 import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
@@ -45,6 +47,7 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.project.CommentLinkInfo;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -64,8 +67,16 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 public class ProjectConfig extends VersionedMetaData {
+  public static final String COMMENTLINK = "commentlink";
+  private static final String KEY_MATCH = "match";
+  private static final String KEY_HTML = "html";
+  private static final String KEY_LINK = "link";
+  private static final String KEY_ENABLED = "enabled";
+
   private static final String PROJECT_CONFIG = "project.config";
   private static final String GROUP_LIST = "groups";
 
@@ -131,6 +142,7 @@
   private Map<String, ContributorAgreement> contributorAgreements;
   private Map<String, NotifyConfig> notifySections;
   private Map<String, LabelType> labelSections;
+  private List<CommentLinkInfo> commentLinkSections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
 
@@ -148,6 +160,42 @@
     return r;
   }
 
+  public static CommentLinkInfo buildCommentLink(Config cfg, String name,
+      boolean allowRaw) throws IllegalArgumentException {
+    String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
+    if (match != null) {
+      // Unfortunately this validation isn't entirely complete. Clients
+      // can have exceptions trying to evaluate the pattern if they don't
+      // support a token used, even if the server does support the token.
+      //
+      // At the minimum, we can trap problems related to unmatched groups.
+      Pattern.compile(match);
+    }
+
+    String link = cfg.getString(COMMENTLINK, name, KEY_LINK);
+    String html = cfg.getString(COMMENTLINK, name, KEY_HTML);
+    boolean hasHtml = !Strings.isNullOrEmpty(html);
+
+    String rawEnabled = cfg.getString(COMMENTLINK, name, KEY_ENABLED);
+    Boolean enabled;
+    if (rawEnabled != null) {
+      enabled = cfg.getBoolean(COMMENTLINK, name, KEY_ENABLED, true);
+    } else {
+      enabled = null;
+    }
+    checkArgument(allowRaw || !hasHtml, "Raw html replacement not allowed");
+
+    if (Strings.isNullOrEmpty(match) && Strings.isNullOrEmpty(link) && !hasHtml
+        && enabled != null) {
+      if (enabled) {
+        return new CommentLinkInfo.Enabled(name);
+      } else {
+        return new CommentLinkInfo.Disabled(name);
+      }
+    }
+    return new CommentLinkInfo(name, match, link, html, enabled);
+  }
+
   public ProjectConfig(Project.NameKey projectName) {
     this.projectName = projectName;
   }
@@ -233,6 +281,10 @@
     return labelSections;
   }
 
+  public Collection<CommentLinkInfo> getCommentLinkSections() {
+    return commentLinkSections;
+  }
+
   public GroupReference resolve(AccountGroup group) {
     return resolve(GroupReference.forGroup(group));
   }
@@ -333,6 +385,7 @@
     loadAccessSections(rc, groupsByName);
     loadNotifySections(rc, groupsByName);
     loadLabelSections(rc);
+    loadCommentLinkSections(rc);
   }
 
   private void loadAccountsSection(
@@ -590,6 +643,25 @@
     }
   }
 
+  private void loadCommentLinkSections(Config rc) {
+    Set<String> subsections = rc.getSubsections(COMMENTLINK);
+    commentLinkSections = Lists.newArrayListWithCapacity(subsections.size());
+    for (String name : subsections) {
+      try {
+        commentLinkSections.add(buildCommentLink(rc, name, false));
+      } catch (PatternSyntaxException e) {
+        error(new ValidationError(PROJECT_CONFIG, String.format(
+            "Invalid pattern \"%s\" in commentlink.%s.match: %s",
+            rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+      } catch (IllegalArgumentException e) {
+        error(new ValidationError(PROJECT_CONFIG, String.format(
+            "Error in pattern \"%s\" in commentlink.%s.match: %s",
+            rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+      }
+    }
+    commentLinkSections = ImmutableList.copyOf(commentLinkSections);
+  }
+
   private Map<String, GroupReference> readGroupList() throws IOException {
     groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
     Map<String, GroupReference> groupsByName =
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..77c4863 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
@@ -25,6 +25,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 +36,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
@@ -77,7 +80,7 @@
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
                     n.patchsetId, n.change,
                     args.mergeUtil.getSubmitter(n.patchsetId).getAccountId(),
-                    newMergeTip, args.mergeUtil);
+                    newMergeTip, args.mergeUtil, committerIdent);
             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 9dd5a4f..2177954 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
@@ -29,11 +29,13 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
@@ -49,6 +51,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -59,6 +62,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 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.RevisionResource;
+import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -87,6 +93,7 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -126,6 +133,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -272,11 +280,15 @@
       new HashMap<Change.Id, ReplaceRequest>();
   private final Map<RevCommit, ReplaceRequest> replaceByCommit =
       new HashMap<RevCommit, ReplaceRequest>();
+  private final Set<RevCommit> validCommits = new HashSet<RevCommit>();
 
+  private ListMultimap<Change.Id, Ref> refsByChange;
   private Map<ObjectId, Ref> refsById;
   private Map<String, Ref> allRefs;
 
   private final SubmoduleOp.Factory subOpFactory;
+  private final Provider<Submit> submitProvider;
+  private final MergeQueue mergeQueue;
 
   private final List<CommitValidationMessage> messages = new ArrayList<CommitValidationMessage>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
@@ -316,7 +328,9 @@
       ReceiveConfig config,
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo,
-      final SubmoduleOp.Factory subOpFactory) throws IOException {
+      final SubmoduleOp.Factory subOpFactory,
+      final Provider<Submit> submitProvider,
+      final MergeQueue mergeQueue) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
     this.schemaFactory = schemaFactory;
@@ -351,6 +365,8 @@
     this.rejectCommits = loadRejectCommitsMap();
 
     this.subOpFactory = subOpFactory;
+    this.submitProvider = submitProvider;
+    this.mergeQueue = mergeQueue;
 
     this.messageSender = new ReceivePackMessageSender();
 
@@ -993,13 +1009,20 @@
     RefControl ctl;
     Set<Account.Id> reviewer = Sets.newLinkedHashSet();
     Set<Account.Id> cc = Sets.newLinkedHashSet();
+    RevCommit baseCommit;
+
+    @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
+    ObjectId base;
 
     @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
     String topic;
 
-    @Option(name = "--draft", usage = "mark new/update changes as draft")
+    @Option(name = "--draft", usage = "mark new/updated changes as draft")
     boolean draft;
 
+    @Option(name = "--submit", usage = "immediately submit the change")
+    boolean submit;
+
     @Option(name = "-r", metaVar = "EMAIL", usage = "add reviewer to changes")
     void reviewer(Account.Id id) {
       reviewer.add(id);
@@ -1024,6 +1047,10 @@
       return draft;
     }
 
+    boolean isSubmit() {
+      return submit;
+    }
+
     MailRecipients getMailRecipients() {
       return new MailRecipients(reviewer, cc);
     }
@@ -1120,13 +1147,41 @@
       return;
     }
 
+    if (magicBranch.isDraft() && magicBranch.isSubmit()) {
+      reject(cmd, "cannot submit draft");
+      return;
+    }
+
+    if (magicBranch.isSubmit() && !projectControl.controlForRef(
+        MagicBranch.NEW_CHANGE + ref).canSubmit()) {
+      reject(cmd, "submit not allowed");
+    }
+
+    RevWalk walk = rp.getRevWalk();
+    if (magicBranch.base != null) {
+      try {
+        magicBranch.baseCommit = walk.parseCommit(magicBranch.base);
+      } catch (IncorrectObjectTypeException notCommit) {
+        reject(cmd, "base must be a commit");
+        return;
+      } catch (MissingObjectException e) {
+        reject(cmd, "base not found");
+        return;
+      } catch (IOException e) {
+        log.warn(String.format(
+            "Project %s cannot read %s",
+            project.getName(), magicBranch.base.name()), e);
+        reject(cmd, "internal server error");
+        return;
+      }
+    }
+
     // Validate that the new commits are connected with the target
     // branch.  If they aren't, we want to abort. We do this check by
     // looking to see if we can compute a merge base between the new
     // commits and the target branch head.
     //
     try {
-      final RevWalk walk = rp.getRevWalk();
       final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId());
       Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
       if (targetRef == null || targetRef.getObjectId() == null) {
@@ -1256,10 +1311,14 @@
     try {
       Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
-      markHeadsAsUninteresting(
-          walk,
-          existing,
-          magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+      if (magicBranch.baseCommit != null) {
+        walk.markUninteresting(magicBranch.baseCommit);
+      } else {
+        markHeadsAsUninteresting(
+            walk,
+            existing,
+            magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+      }
 
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
@@ -1466,8 +1525,8 @@
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
 
-      changeInserter.insertChange(db, change, ps, commit, labelTypes,
-          footerLines, info, recipients.getReviewers());
+      changeInserter.insertChange(db, change, ps, commit, labelTypes, info,
+          recipients.getReviewers());
 
       created = true;
 
@@ -1493,16 +1552,56 @@
           return "send-email newchange";
         }
       }));
+
+      if (magicBranch != null && magicBranch.isSubmit()) {
+        submit(projectControl.controlFor(change), ps);
+      }
+    }
+  }
+
+  private void submit(ChangeControl changeCtl, PatchSet ps) throws OrmException {
+    Submit submit = submitProvider.get();
+    RevisionResource rsrc = new RevisionResource(new ChangeResource(changeCtl), ps);
+    Change c = submit.submit(rsrc, currentUser);
+    if (c == null) {
+      addError("Submitting change " + changeCtl.getChange().getChangeId()
+          + " failed.");
+    } else {
+      addMessage("");
+      mergeQueue.merge(c.getDest());
+      c = db.changes().get(c.getId());
+      switch (c.getStatus()) {
+        case SUBMITTED:
+          addMessage("Change " + c.getChangeId() + " submitted.");
+          break;
+        case MERGED:
+          addMessage("Change " + c.getChangeId() + " merged.");
+          break;
+        case NEW:
+          ChangeMessage msg = submit.getConflictMessage(rsrc);
+          if (msg != null) {
+            addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
+            break;
+          }
+        default:
+          addMessage("change " + c.getChangeId() + " is "
+              + c.getStatus().name().toLowerCase());
+      }
     }
   }
 
   private void preparePatchSetsForReplace() {
     try {
       readChangesForReplace();
-      readPatchSetsForReplace();
-      for (ReplaceRequest req : replaceByChange.values()) {
+      for (Iterator<ReplaceRequest> itr = replaceByChange.values().iterator();
+          itr.hasNext();) {
+        ReplaceRequest req = itr.next();
         if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
           req.validate(false);
+          if (req.skip && req.cmd == null) {
+            itr.remove();
+            replaceByCommit.remove(req.newCommit);
+          }
         }
       }
     } catch (OrmException err) {
@@ -1554,17 +1653,6 @@
     }
   }
 
-  private void readPatchSetsForReplace() throws OrmException {
-    Map<Change.Id, ResultSet<PatchSet>> results = Maps.newHashMap();
-    for (ReplaceRequest request : replaceByChange.values()) {
-      Change.Id id = request.ontoChange;
-      results.put(id, db.patchSets().byChange(id));
-    }
-    for (ReplaceRequest req : replaceByChange.values()) {
-      req.patchSets = results.get(req.ontoChange).toList();
-    }
-  }
-
   private class ReplaceRequest {
     final Change.Id ontoChange;
     final RevCommit newCommit;
@@ -1572,12 +1660,13 @@
     final boolean checkMergedInto;
     Change change;
     ChangeControl changeCtl;
-    List<PatchSet> patchSets;
+    BiMap<RevCommit, PatchSet.Id> revisions;
     PatchSet newPatchSet;
     ReceiveCommand cmd;
     PatchSetInfo info;
     ChangeMessage msg;
     String mergedIntoRef;
+    boolean skip;
     private PatchSet.Id priorPatchSet;
 
     ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
@@ -1586,20 +1675,39 @@
       this.newCommit = newCommit;
       this.inputCommand = cmd;
       this.checkMergedInto = checkMergedInto;
+
+      revisions = HashBiMap.create();
+      for (Ref ref : refs(toChange)) {
+        try {
+          revisions.forcePut(
+              rp.getRevWalk().parseCommit(ref.getObjectId()),
+              PatchSet.Id.fromRef(ref.getName()));
+        } catch (IOException err) {
+          log.warn(String.format(
+              "Project %s contains invalid change ref %s",
+              project.getName(), ref.getName()), err);
+        }
+      }
     }
 
     boolean validate(boolean autoClose) throws IOException {
       if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
         return false;
-      }
-
-      if (change == null || patchSets == null) {
+      } else if (change == null) {
         reject(inputCommand, "change " + ontoChange + " not found");
         return false;
       }
 
-      if (change.getStatus().isClosed()) {
-        reject(inputCommand, "change " + ontoChange + " closed");
+      priorPatchSet = change.currentPatchSetId();
+      if (!revisions.containsValue(priorPatchSet)) {
+        reject(inputCommand, "change " + ontoChange + " missing revisions");
+        return false;
+      }
+
+      RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+      if (newCommit == priorCommit) {
+        // Ignore requests to make the change its current state.
+        skip = true;
         return false;
       }
 
@@ -1607,88 +1715,63 @@
       if (!changeCtl.canAddPatchSet()) {
         reject(inputCommand, "cannot replace " + ontoChange);
         return false;
+      } else if (change.getStatus().isClosed()) {
+        reject(inputCommand, "change " + ontoChange + " closed");
+        return false;
+      } else if (revisions.containsKey(newCommit)) {
+        reject(inputCommand, "commit already exists");
+        return false;
+      }
+
+      for (RevCommit prior : revisions.keySet()) {
+        // Don't allow a change to directly depend upon itself. This is a
+        // very common error due to users making a new commit rather than
+        // amending when trying to address review comments.
+        if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
+          reject(inputCommand, "squash commits first");
+          return false;
+        }
       }
 
       rp.getRevWalk().parseBody(newCommit);
-
       if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) {
         return false;
       }
 
-      priorPatchSet = change.currentPatchSetId();
-      for (final PatchSet ps : patchSets) {
-        if (ps.getRevision() == null) {
-          log.warn("Patch set " + ps.getId() + " has no revision");
-          reject(inputCommand, "change state corrupt");
+      // 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;
-        }
-
-        final String revIdStr = ps.getRevision().get();
-        final ObjectId commitId;
-        try {
-          commitId = ObjectId.fromString(revIdStr);
-        } catch (IllegalArgumentException e) {
-          log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr);
-          reject(inputCommand, "change state corrupt");
-          return false;
-        }
-
-        try {
-          final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
-
-          // Don't allow the same commit to appear twice on the same change
-          //
-          if (newCommit == prior) {
-            reject(inputCommand, "commit already exists");
-            return false;
+        } else {
+          StringBuilder msg = new StringBuilder();
+          msg.append("(W) ");
+          msg.append(reader.abbreviate(newCommit).name());
+          msg.append(":");
+          msg.append(" no files changed");
+          if (!authorEq) {
+            msg.append(", author changed");
           }
-
-          // Don't allow a change to directly depend upon itself. This is a
-          // very common error due to users making a new commit rather than
-          // amending when trying to address review comments.
-          //
-          if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
-            reject(inputCommand, "squash commits first");
-            return false;
+          if (!messageEq) {
+            msg.append(", message updated");
           }
-
-          // 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 (priorPatchSet.equals(ps.getId()) && newCommit.getTree() == prior.getTree()) {
-            rp.getRevWalk().parseBody(prior);
-            final boolean messageEq =
-                eq(newCommit.getFullMessage(), prior.getFullMessage());
-            final boolean parentsEq = parentsEqual(newCommit, prior);
-            final boolean authorEq = authorEqual(newCommit, prior);
-
-            if (messageEq && parentsEq && authorEq && !autoClose) {
-              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());
-              msg.append(":");
-              msg.append(" no files changed");
-              if (!authorEq) {
-                msg.append(", author changed");
-              }
-              if (!messageEq) {
-                msg.append(", message updated");
-              }
-              if (!parentsEq) {
-                msg.append(", was rebased");
-              }
-              addMessage(msg.toString());
-            }
+          if (!parentsEq) {
+            msg.append(", was rebased");
           }
-        } catch (IOException e) {
-          log.error("Change " + change.getId() + " missing " + revIdStr, e);
-          reject(inputCommand, "change state corrupt");
-          return false;
+          addMessage(msg.toString());
         }
       }
 
@@ -1765,9 +1848,12 @@
           mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
         }
 
-        final MailRecipients oldRecipients =
-            getRecipientsFromApprovals(ApprovalsUtil.copyLabels(
-                db, labelTypes, priorPatchSet, newPatchSet.getId()));
+        List<PatchSetApproval> oldChangeApprovals =
+            db.patchSetApprovals().byChange(change.getId()).toList();
+        final MailRecipients oldRecipients = getRecipientsFromApprovals(
+            oldChangeApprovals);
+        ApprovalsUtil.copyLabels(db, labelTypes, oldChangeApprovals,
+            priorPatchSet, newPatchSet.getId());
         approvalsUtil.addReviewers(db, labelTypes, change, newPatchSet, info,
             recipients.getReviewers(), oldRecipients.getAll());
         recipients.add(oldRecipients);
@@ -1865,10 +1951,30 @@
           return "send-email newpatchset";
         }
       }));
+
+      if (magicBranch != null && magicBranch.isSubmit()) {
+        submit(changeCtl, newPatchSet);
+      }
+
       return newPatchSet.getId();
     }
   }
 
+  private List<Ref> refs(Change.Id changeId) {
+    if (refsByChange == null) {
+      int estRefsPerChange = 4;
+      refsByChange = ArrayListMultimap.create(
+          allRefs.size() / estRefsPerChange,
+          estRefsPerChange);
+      for (Ref ref : allRefs.values()) {
+        if (ref.getObjectId() != null && PatchSet.isRef(ref.getName())) {
+          refsByChange.put(Change.Id.fromRef(ref.getName()), ref);
+        }
+      }
+    }
+    return refsByChange.get(changeId);
+  }
+
   static boolean parentsEqual(RevCommit a, RevCommit b) {
     if (a.getParentCount() != b.getParentCount()) {
       return false;
@@ -1970,6 +2076,10 @@
   private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd,
       final RevCommit c) throws MissingObjectException, IOException {
 
+    if (validCommits.contains(c)) {
+      return true;
+    }
+
     CommitReceivedEvent receiveEvent =
         new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, currentUser);
     CommitValidators commitValidators =
@@ -1982,6 +2092,7 @@
       reject(cmd, e.getMessage());
       return false;
     }
+    validCommits.add(c);
     return true;
   }
 
@@ -2017,7 +2128,6 @@
           if (onto != null) {
             final ReplaceRequest req = new ReplaceRequest(onto, c, cmd, false);
             req.change = db.changes().get(onto);
-            req.patchSets = db.patchSets().byChange(onto).toList();
             toClose.add(req);
             break;
           }
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..d43a756 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
@@ -93,7 +93,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/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 97af5ac..c6fc709 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,13 @@
 
 package com.google.gerrit.server.git.validators;
 
+import com.google.common.base.CharMatcher;
 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;
@@ -33,6 +35,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;
@@ -60,6 +63,9 @@
   private static final Pattern NEW_PATCHSET = Pattern
       .compile("^refs/changes/(?:[0-9][0-9])?(/[1-9][0-9]*){1,2}(?:/new)?$");
 
+  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);
@@ -68,6 +74,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;
@@ -75,12 +82,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;
@@ -100,7 +110,8 @@
     validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
     if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
         || 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));
@@ -133,7 +144,8 @@
     validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
     if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
         || 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));
@@ -154,23 +166,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 =
@@ -179,8 +192,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) {
@@ -191,13 +204,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);
+    }
   }
 
   /**
@@ -449,92 +548,6 @@
   }
 
   /**
-   * 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 footer 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 = ".git/hooks/commit-msg";
-      return String.format(
-          "  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("  scp -p -P %d %s@%s:hooks/commit-msg .git/hooks/",
-        sshPort, currentUser.getUserName(), sshHost);
-  }
-
-  /**
    * Get the Gerrit URL.
    *
    * @return the canonical URL (with any trailing slash removed) if it is
@@ -543,10 +556,7 @@
    */
   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/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index d32c632..28f908c 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
@@ -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
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 885f1aa..39d0ff8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -329,6 +329,11 @@
         add(RecipientType.CC, ap.getAccountId());
       }
     } catch (OrmException err) {
+      if (includeZero) {
+        log.warn("Cannot CC users that commented on updated change", err);
+      } else {
+        log.warn("Cannot CC users that reviewed updated change", err);
+      }
     }
   }
 
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 997bc03..ce50002 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
@@ -268,7 +268,7 @@
   protected boolean shouldSendMessage() {
     if (body.length() == 0) {
       // If we have no message body, don't send.
-      //
+      log.warn("Skipping delivery of email with no body");
       return false;
     }
 
@@ -276,7 +276,7 @@
       // If we have nobody to send this message to, then all of our
       // selection filters previously for this type of message were
       // unable to match a destination. Don't bother sending it.
-      //
+      log.info("Skipping delivery of email with no recipients");
       return false;
     }
 
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 985272a..8e7192e 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
@@ -21,6 +21,8 @@
 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;
@@ -83,6 +85,7 @@
 
   private final CacheKey cacheKey;
   private final String name;
+  private final PluginUser pluginUser;
   private final File srcJar;
   private final FileSnapshot snapshot;
   private final JarFile jarFile;
@@ -102,6 +105,7 @@
   private List<ReloadableRegistrationHandle<?>> reloadableHandles;
 
   public Plugin(String name,
+      PluginUser pluginUser,
       File srcJar,
       FileSnapshot snapshot,
       JarFile jarFile,
@@ -113,6 +117,7 @@
       @Nullable Class<? extends Module> sshModule,
       @Nullable Class<? extends Module> httpModule) {
     this.cacheKey = new CacheKey(name);
+    this.pluginUser = pluginUser;
     this.name = name;
     this.srcJar = srcJar;
     this.snapshot = snapshot;
@@ -131,6 +136,10 @@
     return srcJar;
   }
 
+  PluginUser getPluginUser() {
+    return pluginUser;
+  }
+
   public CacheKey getCacheKey() {
     return cacheKey;
   }
@@ -173,6 +182,15 @@
   }
 
   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();
 
@@ -235,6 +253,7 @@
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
+        bind(PluginUser.class).toInstance(pluginUser);
         bind(String.class)
           .annotatedWith(PluginName.class)
           .toInstance(name);
@@ -264,9 +283,14 @@
     return Guice.createInjector(modules);
   }
 
-  void stop() {
+  void stop(PluginGuiceEnvironment env) {
     if (manager != null) {
-      manager.stop();
+      RequestContext oldContext = env.enter(this);
+      try {
+        manager.stop();
+      } finally {
+        env.exit(oldContext);
+      }
       manager = null;
       sysInjector = null;
       sshInjector = null;
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 664c278..387ffa4 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
@@ -32,6 +32,9 @@
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.util.PluginRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.AbstractModule;
 import com.google.inject.Binding;
 import com.google.inject.Guice;
@@ -65,6 +68,7 @@
 public class PluginGuiceEnvironment {
   private final Injector sysInjector;
   private final ServerInformation srvInfo;
+  private final ThreadLocalRequestContext local;
   private final CopyConfigModule copyConfigModule;
   private final Set<Key<?>> copyConfigKeys;
   private final List<StartPluginListener> onStart;
@@ -90,10 +94,12 @@
   @Inject
   PluginGuiceEnvironment(
       Injector sysInjector,
+      ThreadLocalRequestContext local,
       ServerInformation srvInfo,
       CopyConfigModule ccm) {
     this.sysInjector = sysInjector;
     this.srvInfo = srvInfo;
+    this.local = local;
     this.copyConfigModule = ccm;
     this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
 
@@ -187,20 +193,33 @@
     return httpGen.get();
   }
 
+  RequestContext enter(Plugin plugin) {
+    return local.setContext(new PluginRequestContext(plugin.getPluginUser()));
+  }
+
+  void exit(RequestContext old) {
+    local.setContext(old);
+  }
+
   void onStartPlugin(Plugin plugin) {
     for (StartPluginListener l : onStart) {
       l.onStartPlugin(plugin);
     }
 
-    attachItem(sysItems, plugin.getSysInjector(), plugin);
+    RequestContext oldContext = enter(plugin);
+    try {
+      attachItem(sysItems, plugin.getSysInjector(), plugin);
 
-    attachSet(sysSets, plugin.getSysInjector(), plugin);
-    attachSet(sshSets, plugin.getSshInjector(), plugin);
-    attachSet(httpSets, plugin.getHttpInjector(), plugin);
+      attachSet(sysSets, plugin.getSysInjector(), plugin);
+      attachSet(sshSets, plugin.getSshInjector(), plugin);
+      attachSet(httpSets, plugin.getHttpInjector(), plugin);
 
-    attachMap(sysMaps, plugin.getSysInjector(), plugin);
-    attachMap(sshMaps, plugin.getSshInjector(), plugin);
-    attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+      attachMap(sysMaps, plugin.getSysInjector(), plugin);
+      attachMap(sshMaps, plugin.getSshInjector(), plugin);
+      attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+    } finally {
+      exit(oldContext);
+    }
   }
 
   private void attachItem(Map<TypeLiteral<?>, DynamicItem<?>> items,
@@ -244,15 +263,20 @@
       old.put(h.getKey().getTypeLiteral(), h);
     }
 
-    reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
-    reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
-    reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
+    RequestContext oldContext = enter(newPlugin);
+    try {
+      reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
+      reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
+      reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
 
-    reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
-    reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
-    reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
+      reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
+      reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
+      reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
 
-    reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin);
+      reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin);
+    } finally {
+      exit(oldContext);
+    }
   }
 
   private void reattachMap(
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 f832c8a..035592c 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -50,7 +51,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
-import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -72,6 +72,7 @@
   private final File tmpDir;
   private final PluginGuiceEnvironment env;
   private final ServerInformationImpl srvInfoImpl;
+  private final PluginUser.Factory pluginUserFactory;
   private final ConcurrentMap<String, Plugin> running;
   private final ConcurrentMap<String, Plugin> disabled;
   private final Map<String, FileSnapshot> broken;
@@ -84,6 +85,7 @@
   public PluginLoader(SitePaths sitePaths,
       PluginGuiceEnvironment pe,
       ServerInformationImpl sii,
+      PluginUser.Factory puf,
       Provider<PluginCleanerTask> pct,
       @GerritServerConfig Config cfg) {
     pluginsDir = sitePaths.plugins_dir;
@@ -91,11 +93,12 @@
     tmpDir = sitePaths.tmp_dir;
     env = pe;
     srvInfoImpl = sii;
+    pluginUserFactory = puf;
     running = Maps.newConcurrentMap();
     disabled = Maps.newConcurrentMap();
     broken = Maps.newHashMap();
     toCleanup = Queues.newArrayDeque();
-    cleanupHandles = new Hashtable<Plugin,CleanupHandle>();
+    cleanupHandles = Maps.newConcurrentMap();
     cleaner = pct;
 
     long checkFrequency = ConfigUtil.getTimeUnit(cfg,
@@ -194,7 +197,7 @@
   synchronized private void unloadPlugin(Plugin plugin) {
     String name = plugin.getName();
     log.info(String.format("Unloading plugin %s", name));
-    plugin.stop();
+    plugin.stop(env);
     running.remove(name);
     disabled.remove(name);
     toCleanup.add(plugin);
@@ -466,7 +469,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,
+      Plugin plugin = new Plugin(name, pluginUserFactory.create(name),
           srcJar, snapshot,
           jarFile, manifest,
           new File(dataDir, name), type, pluginLoader,
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..110fe15
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.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.project;
+
+import com.google.gerrit.extensions.restapi.RestView;
+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;
+  }
+}
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 48b0731..d09dcb7 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
@@ -36,11 +36,13 @@
 import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -55,10 +57,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)
@@ -68,8 +72,34 @@
         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.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 {
@@ -327,7 +357,6 @@
     return canSubmit(db, patchSet, null, false, false, false);
   }
 
-  @SuppressWarnings("unchecked")
   public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
       @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed,
       boolean allowDraft) {
@@ -355,7 +384,7 @@
           fastEvalLabels,
           "locate_submit_rule", "can_submit",
           "locate_submit_filter", "filter_submit_results");
-      results = evaluator.evaluate().toJava();
+      results = evaluator.evaluate();
     } catch (RuleEvalException e) {
       return logRuleError(e.getMessage(), e);
     }
@@ -475,7 +504,6 @@
     return getSubmitTypeRecord(db, patchSet, null);
   }
 
-  @SuppressWarnings("unchecked")
   public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet,
       @Nullable ChangeData cd) {
     try {
@@ -492,7 +520,7 @@
           err);
     }
 
-    List<String> results;
+    List<Term> results;
     SubmitRuleEvaluator evaluator;
     try {
       evaluator = new SubmitRuleEvaluator(db, patchSet,
@@ -500,7 +528,7 @@
           false,
           "locate_submit_type", "get_submit_type",
           "locate_submit_type_filter", "filter_submit_type_results");
-      results = evaluator.evaluate().toJava();
+      results = evaluator.evaluate();
     } catch (RuleEvalException e) {
       return logTypeRuleError(e.getMessage(), e);
     }
@@ -513,10 +541,15 @@
       return typeRuleError("Project submit rule has no solution");
     }
 
-    // Take only the first result and convert it to SubmitTypeRecord
-    // This logic will need to change once we support multiple submit types
-    // in the UI
-    String typeName = results.get(0);
+    Term typeTerm = results.get(0);
+    if (!typeTerm.isSymbol()) {
+      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
+          + change.getId() + " of " + getProject().getName()
+          + " did not return a symbol.");
+      return typeRuleError("Project submit rule has invalid solution");
+    }
+
+    String typeName = ((SymbolTerm)typeTerm).name();
     try {
       return SubmitTypeRecord.OK(
           Project.SubmitType.valueOf(typeName.toUpperCase()));
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/CommentLinkInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
new file mode 100644
index 0000000..4035c7e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
@@ -0,0 +1,92 @@
+// 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.project;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Strings;
+
+/** Info about a single commentlink section in a config. */
+public class CommentLinkInfo {
+  public static class Enabled extends CommentLinkInfo {
+    public Enabled(String name) {
+      super(name, true);
+    }
+
+    @Override
+    boolean isOverrideOnly() {
+      return true;
+    }
+  }
+
+  public static class Disabled extends CommentLinkInfo {
+    public Disabled(String name) {
+      super(name, false);
+    }
+
+    @Override
+    boolean isOverrideOnly() {
+      return true;
+    }
+  }
+
+  public final String match;
+  public final String link;
+  public final String html;
+  public final Boolean enabled; // null means true
+
+  public transient final String name;
+
+  public CommentLinkInfo(String name, String match, String link, String html,
+      Boolean enabled) {
+    checkArgument(name != null, "invalid commentlink.name");
+    checkArgument(!Strings.isNullOrEmpty(match),
+        "invalid commentlink.%s.match", name);
+    link = Strings.emptyToNull(link);
+    html = Strings.emptyToNull(html);
+    checkArgument(
+        (link != null && html == null) || (link == null && html != null),
+        "commentlink.%s must have either link or html", name);
+    this.name = name;
+    this.match = match;
+    this.link = link;
+    this.html = html;
+    this.enabled = enabled;
+  }
+
+  private CommentLinkInfo(CommentLinkInfo src, boolean enabled) {
+    this.name = src.name;
+    this.match = src.match;
+    this.link = src.link;
+    this.html = src.html;
+    this.enabled = enabled;
+  }
+
+  private CommentLinkInfo(String name, boolean enabled) {
+    this.name = name;
+    this.match = null;
+    this.link = null;
+    this.html = null;
+    this.enabled = enabled;
+  }
+
+  boolean isOverrideOnly() {
+    return false;
+  }
+
+  CommentLinkInfo inherit(CommentLinkInfo src) {
+    return new CommentLinkInfo(src, enabled);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
new file mode 100644
index 0000000..114ab90
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -0,0 +1,53 @@
+// 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.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.List;
+import java.util.Set;
+
+public class CommentLinkProvider implements Provider<List<CommentLinkInfo>> {
+  private final Config cfg;
+
+  @Inject
+  CommentLinkProvider(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
+  }
+
+  @Override
+  public List<CommentLinkInfo> get() {
+    Set<String> subsections = cfg.getSubsections(ProjectConfig.COMMENTLINK);
+    List<CommentLinkInfo> cls =
+        Lists.newArrayListWithCapacity(subsections.size());
+    for (String name : subsections) {
+      CommentLinkInfo cl = ProjectConfig.buildCommentLink(cfg, name, true);
+      if (cl.isOverrideOnly()) {
+        throw new ProvisionException(
+            "commentlink " + name + " empty except for \"enabled\"");
+      }
+      cls.add(cl);
+    }
+    return ImmutableList.copyOf(cls);
+  }
+}
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..5985de0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -0,0 +1,234 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.errors.InvalidRevisionException;
+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, 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 IllegalStateException("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.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-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..a853c75 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
@@ -35,6 +35,7 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import java.io.IOException;
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
@@ -79,7 +80,7 @@
   @Override
   public Object apply(TopLevelResource resource, Input input)
       throws BadRequestException, UnprocessableEntityException,
-      ProjectCreationFailedException {
+      ProjectCreationFailedException, IOException {
     if (input == null) {
       input = new Input();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index 38690d1..a50e71c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -160,8 +160,8 @@
     DashboardInfo info = new DashboardInfo(refName, path);
     info.project = project;
     info.definingProject = definingProject.getName();
-    info.title = config.getString("dashboard", null, "title");
-    info.description = config.getString("dashboard", null, "description");
+    info.title = replace(project, config.getString("dashboard", null, "title"));
+    info.description = replace(project, config.getString("dashboard", null, "description"));
     info.foreach = config.getString("dashboard", null, "foreach");
 
     if (setDefault) {
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..5b19ff0 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,7 @@
           writer.flush();
         }
       }
-    };
+    }.setContentType("text/plain; charset=UTF-8")
+     .disableGzip();
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
similarity index 64%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
index b2fb901..781cf01 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
@@ -12,12 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.restapi;
+package com.google.gerrit.server.project;
 
-import java.io.IOException;
-import java.io.OutputStream;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
 
-public interface StreamingResponse {
-  public String getContentType();
-  public void stream(OutputStream out) throws IOException;
+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
new file mode 100644
index 0000000..3ae78b9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.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.project;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import java.util.Map;
+
+public class GetConfig implements RestReadView<ProjectResource> {
+  public static class ConfigInfo {
+    public final String kind = "gerritcodereview#project_config";
+
+    public Boolean useContributorAgreements;
+    public Boolean useContentMerge;
+    public Boolean useSignedOffBy;
+    public Boolean requireChangeId;
+
+    public Map<String, CommentLinkInfo> commentlinks;
+    public ThemeInfo theme;
+  }
+
+  @Override
+  public ConfigInfo apply(ProjectResource resource) {
+    ConfigInfo result = new ConfigInfo();
+    RefControl refConfig = resource.getControl()
+        .controlForRef(GitRepositoryManager.REF_CONFIG);
+    ProjectState project = resource.getControl().getProjectState();
+    if (refConfig.isVisible()) {
+      result.useContributorAgreements = project.isUseContributorAgreements();
+      result.useContentMerge = project.isUseContentMerge();
+      result.useSignedOffBy = project.isUseSignedOffBy();
+      result.requireChangeId = project.isRequireChangeID();
+    }
+
+    // commentlinks are visible to anyone, as they are used for linkification
+    // on the client side.
+    result.commentlinks = Maps.newLinkedHashMap();
+    for (CommentLinkInfo cl : project.getCommentLinks()) {
+      result.commentlinks.put(cl.name, cl);
+    }
+
+    // Themes are visible to anyone, as they are rendered client-side.
+    result.theme = project.getTheme();
+    return result;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
index a9a226d..548b85a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
@@ -29,7 +29,6 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
-import java.util.Properties;
 
 @RequiresCapability(GlobalCapability.RUN_GC)
 public class GetStatistics implements RestReadView<ProjectResource> {
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..b01c757
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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.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 = 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 94e3162..3b44f66 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,16 +43,26 @@
     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);
+    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);
     delete(DASHBOARD_KIND).to(DeleteDashboard.class);
     install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
+
+    get(PROJECT_KIND, "config").to(GetConfig.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 dd8722f..d68725f 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
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
@@ -73,17 +72,16 @@
   private final GitReferenceUpdated referenceUpdated;
   private final DynamicSet<NewProjectCreatedListener> createdListener;
   private final PersonIdent serverIdent;
-  private CreateProjectArgs createProjectArgs;
-  private ProjectCache projectCache;
-  private GroupBackend groupBackend;
-  private MetaDataUpdate.User metaDataUpdateFactory;
+  private final CreateProjectArgs createProjectArgs;
+  private final ProjectCache projectCache;
+  private final GroupBackend groupBackend;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
 
   @Inject
   PerformCreateProject(@ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
       IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
       GitReferenceUpdated referenceUpdated,
       DynamicSet<NewProjectCreatedListener> createdListener,
-      ReviewDb db,
       @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
       MetaDataUpdate.User metaDataUpdateFactory,
       @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
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 56d4be3..3f107ec 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
@@ -38,6 +38,7 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -62,8 +63,8 @@
     }
 
     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);
       }
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 6150a3f..6f80841 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
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Charsets;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.io.Files;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
@@ -34,6 +37,7 @@
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
+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.Inject;
@@ -44,7 +48,10 @@
 
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -58,17 +65,22 @@
 
 /** Cached information on a project. */
 public class ProjectState {
+  private static final Logger log =
+      LoggerFactory.getLogger(ProjectState.class);
+
   public interface Factory {
     ProjectState create(ProjectConfig config);
   }
 
   private final boolean isAllProjects;
+  private final SitePaths sitePaths;
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
   private final ProjectControl.AssistedFactory projectControlFactory;
   private final PrologEnvironment.Factory envFactory;
   private final GitRepositoryManager gitMgr;
   private final RulesCache rulesCache;
+  private final List<CommentLinkInfo> commentLinks;
 
   private final ProjectConfig config;
   private final Set<AccountGroup.UUID> localOwners;
@@ -82,18 +94,24 @@
   /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
   private volatile List<SectionMatcher> localAccessSections;
 
+  /** Theme information loaded from site_path/themes. */
+  private volatile ThemeInfo theme;
+
   /** If this is all projects, the capabilities used by the server. */
   private final CapabilityCollection capabilities;
 
   @Inject
   public ProjectState(
+      final SitePaths sitePaths,
       final ProjectCache projectCache,
       final AllProjectsName allProjectsName,
       final ProjectControl.AssistedFactory projectControlFactory,
       final PrologEnvironment.Factory envFactory,
       final GitRepositoryManager gitMgr,
       final RulesCache rulesCache,
+      final List<CommentLinkInfo> commentLinks,
       @Assisted final ProjectConfig config) {
+    this.sitePaths = sitePaths;
     this.projectCache = projectCache;
     this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
     this.allProjectsName = allProjectsName;
@@ -101,6 +119,7 @@
     this.envFactory = envFactory;
     this.gitMgr = gitMgr;
     this.rulesCache = rulesCache;
+    this.commentLinks = commentLinks;
     this.config = config;
     this.capabilities = isAllProjects
       ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
@@ -293,6 +312,16 @@
   }
 
   /**
+   * @return an iterable that walks in-order from All-Projects through the
+   *     project hierarchy to this project.
+   */
+  public Iterable<ProjectState> treeInOrder() {
+    List<ProjectState> projects = Lists.newArrayList(tree());
+    Collections.reverse(projects);
+    return projects;
+  }
+
+  /**
    * @return an iterable that walks through the parents of this project. Starts
    *         from the immediate parent of this project and progresses up the
    *         hierarchy to All-Projects.
@@ -343,9 +372,7 @@
 
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
-    List<ProjectState> projects = Lists.newArrayList(tree());
-    Collections.reverse(projects);
-    for (ProjectState s : projects) {
+    for (ProjectState s : treeInOrder()) {
       for (LabelType type : s.getConfig().getLabelSections().values()) {
         String lower = type.getName().toLowerCase();
         LabelType old = types.get(lower);
@@ -363,6 +390,69 @@
     return new LabelTypes(Collections.unmodifiableList(all));
   }
 
+  public List<CommentLinkInfo> getCommentLinks() {
+    Map<String, CommentLinkInfo> cls = Maps.newLinkedHashMap();
+    for (CommentLinkInfo cl : commentLinks) {
+      cls.put(cl.name.toLowerCase(), cl);
+    }
+    for (ProjectState s : treeInOrder()) {
+      for (CommentLinkInfo cl : s.getConfig().getCommentLinkSections()) {
+        String name = cl.name.toLowerCase();
+        if (cl.isOverrideOnly()) {
+          CommentLinkInfo parent = cls.get(name);
+          if (parent == null) {
+            continue; // Ignore invalid overrides.
+          }
+          cls.put(name, cl.inherit(parent));
+        } else {
+          cls.put(name, cl);
+        }
+      }
+    }
+    return ImmutableList.copyOf(cls.values());
+  }
+
+  public ThemeInfo getTheme() {
+    ThemeInfo theme = this.theme;
+    if (theme == null) {
+      synchronized (this) {
+        theme = this.theme;
+        if (theme == null) {
+          theme = loadTheme();
+          this.theme = theme;
+        }
+      }
+    }
+    if (theme == ThemeInfo.INHERIT) {
+      ProjectState parent = Iterables.getFirst(parents(), null);
+      return parent != null ? parent.getTheme() : null;
+    }
+    return theme;
+  }
+
+  private ThemeInfo loadTheme() {
+    String name = getConfig().getProject().getName();
+    File dir = new File(sitePaths.themes_dir, name);
+    if (!dir.exists()) {
+      return ThemeInfo.INHERIT;
+    } else if (!dir.isDirectory()) {
+      log.warn("Bad theme for {}: not a directory", name);
+      return ThemeInfo.INHERIT;
+    }
+    try {
+      return new ThemeInfo(readFile(new File(dir, SitePaths.CSS_FILENAME)),
+          readFile(new File(dir, SitePaths.HEADER_FILENAME)),
+          readFile(new File(dir, SitePaths.FOOTER_FILENAME)));
+    } catch (IOException e) {
+      log.error("Error reading theme for " + name, e);
+      return ThemeInfo.INHERIT;
+    }
+  }
+
+  private String readFile(File f) throws IOException {
+    return f.exists() ? Files.toString(f, Charsets.UTF_8) : null;
+  }
+
   private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
     for (ProjectState s : tree()) {
       switch (func.apply(s.getProject())) {
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/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index bcea2c9..349567c 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+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.server.ReviewDb;
@@ -30,6 +31,7 @@
 
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import javax.annotation.Nullable;
@@ -124,8 +126,7 @@
    * @return List of {@link Term} objects returned from the evaluated rules.
    * @throws RuleEvalException
    */
-  public ListTerm evaluate() throws RuleEvalException {
-    List<Term> results = new ArrayList<Term>();
+  public List<Term> evaluate() throws RuleEvalException {
     PrologEnvironment env = getPrologEnvironment();
     try {
       submitRule = env.once("gerrit", userRuleLocatorName, new VariableTerm());
@@ -133,6 +134,7 @@
         env.once("gerrit", "assume_range_from_label");
       }
 
+      List<Term> results = new ArrayList<Term>();
       try {
         for (Term[] template : env.all("gerrit", userRuleWrapperName,
             submitRule, new VariableTerm())) {
@@ -152,7 +154,16 @@
       if (!skipFilters) {
         resultsTerm = runSubmitFilters(resultsTerm, env);
       }
-      return (ListTerm) resultsTerm;
+      if (resultsTerm.isList()) {
+        List<Term> r = Lists.newArrayList();
+        for (Term t = resultsTerm; t.isList();) {
+          ListTerm l = (ListTerm) t;
+          r.add(l.car().dereference());
+          t = l.cdr().dereference();
+        }
+        return r;
+      }
+      return Collections.emptyList();
     } finally {
       env.close();
     }
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
new file mode 100644
index 0000000..8362b572
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.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.git;
+
+package com.google.gerrit.server.project;
+
+public class ThemeInfo {
+  static final ThemeInfo INHERIT = new ThemeInfo(null, null, null);
+
+  public final String css;
+  public final String header;
+  public final String footer;
+
+  ThemeInfo(String css, String header, String footer) {
+    this.css = css;
+    this.header = header;
+    this.footer = footer;
+  }
+}
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..bf3b605 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
@@ -87,6 +87,13 @@
     }
   }
 
+  public static void ensureAllPatchSetsLoaded(Provider<ReviewDb> db,
+      List<ChangeData> changes) throws OrmException {
+    for (ChangeData cd : changes) {
+      cd.patches(db);
+    }
+  }
+
   public static void ensureCurrentPatchSetLoaded(
       Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
     Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
@@ -141,6 +148,7 @@
   private ChangeControl changeControl;
   private List<ChangeMessage> messages;
   private List<SubmitRecord> submitRecords;
+  private boolean patchesLoaded;
 
   public ChangeData(final Change.Id id) {
     legacyId = id;
@@ -321,7 +329,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 +340,7 @@
       } else {
         patches = db.get().patchSets().byChange(legacyId).toList();
       }
+      patchesLoaded = true;
     }
     return patches;
   }
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 7bbb073..e74172e 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
@@ -367,7 +367,7 @@
 
     // If its not an account, maybe its a group?
     //
-    Collection<GroupReference> suggestions = args.groupBackend.suggest(who);
+    Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
     if (!suggestions.isEmpty()) {
       HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
       for (GroupReference ref : suggestions) {
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 f016c37..0ea280d 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
@@ -14,21 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
@@ -42,78 +33,31 @@
  * Predicate to match changes that contains specified text in commit messages
  * body.
  */
-public class MessagePredicate extends OperatorPredicate<ChangeData> {
+public class MessagePredicate extends RevWalkPredicate {
 
   private static final Logger log =
       LoggerFactory.getLogger(MessagePredicate.class);
 
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager repoManager;
   private final RevFilter rFilter;
 
   public MessagePredicate(Provider<ReviewDb> db,
       GitRepositoryManager repoManager, String text) {
-    super(ChangeQueryBuilder.FIELD_MESSAGE, text);
-    this.db = db;
-    this.repoManager = repoManager;
+    super(db, repoManager, ChangeQueryBuilder.FIELD_MESSAGE, text);
     this.rFilter = MessageRevFilter.create(text);
   }
 
   @Override
-  public boolean match(ChangeData object) throws OrmException {
-    final PatchSet patchSet = object.currentPatchSet(db);
-
-    if (patchSet == null) {
-      return false;
-    }
-
-    final RevId revision = patchSet.getRevision();
-
-    if (revision == null) {
-      return false;
-    }
-
-    final AnyObjectId objectId = ObjectId.fromString(revision.get());
-
-    if (objectId == null) {
-      return false;
-    }
-
-    final Change change = object.change(db);
-
-    if (change == null) {
-      return false;
-    }
-
-    final Project.NameKey projectName = change.getProject();
-
-    if (projectName == null) {
-      return false;
-    }
-
+  public boolean match(Repository repo, RevWalk rw, Arguments args) {
     try {
-      final Repository repo = repoManager.openRepository(projectName);
-      try {
-        final RevWalk rw = new RevWalk(repo);
-        try {
-          return rFilter.include(rw, rw.parseCommit(objectId));
-        } finally {
-          rw.release();
-        }
-      } finally {
-        repo.close();
-      }
-    } catch (RepositoryNotFoundException e) {
-      log.error("Repository \"" + projectName.get() + "\" unknown.", e);
+      return rFilter.include(rw, rw.parseCommit(args.objectId));
     } catch (MissingObjectException e) {
-      log.error(projectName.get() + "\" commit does not exist.", e);
+      log.error(args.projectName.get() + "\" commit does not exist.", e);
     } catch (IncorrectObjectTypeException e) {
-      log.error(projectName.get() + "\" revision is not a commit.", e);
+      log.error(args.projectName.get() + "\" revision is not a commit.", e);
     } catch (IOException e) {
-      log.error("Could not search for commit message in \"" + projectName.get()
-          + "\" repository.", e);
+      log.error("Could not search for commit message in \"" +
+          args.projectName.get() + "\" repository.", e);
     }
-
     return false;
   }
 
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 fc35df3..06d84c1 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
@@ -21,10 +21,10 @@
 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.events.ChangeAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.QueryStatsAttribute;
 import com.google.gerrit.server.events.EventFactory;
-import com.google.gerrit.server.events.PatchSetAttribute;
-import com.google.gerrit.server.events.QueryStats;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -267,7 +267,7 @@
       }
 
       try {
-        final QueryStats stats = new QueryStats();
+        final QueryStatsAttribute stats = new QueryStatsAttribute();
         stats.runTimeMilliseconds = System.currentTimeMillis();
 
         List<ChangeData> results = queryChanges(queryString);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
new file mode 100644
index 0000000..58c0fee
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.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.query.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Predicate which creates Repository, RevWalk objects and properly
+ * closes them. Git based operators should extend this predicate.
+ *
+ */
+public abstract class RevWalkPredicate extends OperatorPredicate<ChangeData> {
+  private static final Logger log =
+      LoggerFactory.getLogger(RevWalkPredicate.class);
+
+  public static class Arguments {
+    public final PatchSet patchSet;
+    public final RevId revision;
+    public final AnyObjectId objectId;
+    public final Change change;
+    public final Project.NameKey projectName;
+
+    public Arguments(PatchSet patchSet,
+        RevId revision,
+        AnyObjectId objectId,
+        Change change,
+        Project.NameKey projectName) {
+      this.patchSet = patchSet;
+      this.revision = revision;
+      this.objectId = objectId;
+      this.change = change;
+      this.projectName = projectName;
+    }
+  }
+
+  public final Provider<ReviewDb> db;
+  public final GitRepositoryManager repoManager;
+
+  public RevWalkPredicate(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager, String operator, String ref) {
+    super(operator, ref);
+    this.db = db;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    final PatchSet patchSet = object.currentPatchSet(db);
+    if (patchSet == null) {
+      return false;
+    }
+
+    final RevId revision = patchSet.getRevision();
+    if (revision == null) {
+      return false;
+    }
+
+    final AnyObjectId objectId = ObjectId.fromString(revision.get());
+    if (objectId == null) {
+      return false;
+    }
+
+    Change change = object.change(db);
+    if (change == null) {
+      return false;
+    }
+
+    final Project.NameKey projectName = change.getProject();
+    if (projectName == null) {
+      return false;
+    }
+
+    Arguments args = new Arguments(patchSet, revision, objectId, change, projectName);
+
+    try {
+      final Repository repo = repoManager.openRepository(projectName);
+      try {
+        final RevWalk rw = new RevWalk(repo);
+        try {
+          return match(repo, rw, args);
+        } finally {
+          rw.release();
+        }
+      } finally {
+        repo.close();
+      }
+    } catch (RepositoryNotFoundException e) {
+      log.error("Repository \"" + projectName.get() + "\" unknown.", e);
+    } catch (IOException e) {
+      log.error(projectName.get() + " cannot be read as a repository", e);
+    }
+    return false;
+  }
+
+  public abstract boolean match(Repository repo, RevWalk rw, Arguments args);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
new file mode 100644
index 0000000..ef8f807
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.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.schema;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+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.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+/** Creates the {@code All-Projects} repository and initial ACLs. */
+public class AllProjectsCreator {
+  private final GitRepositoryManager mgr;
+  private final AllProjectsName allProjectsName;
+  private final PersonIdent serverUser;
+
+  private GroupReference admin;
+  private GroupReference batch;
+  private GroupReference anonymous;
+  private GroupReference registered;
+  private GroupReference owners;
+
+  @Inject
+  AllProjectsCreator(
+      GitRepositoryManager mgr,
+      AllProjectsName allProjectsName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    this.mgr = mgr;
+    this.allProjectsName = allProjectsName;
+    this.serverUser = serverUser;
+
+    this.anonymous = new GroupReference(
+        AccountGroup.ANONYMOUS_USERS,
+        "Anonymous Users");
+    this.registered = new GroupReference(
+        AccountGroup.REGISTERED_USERS,
+        "Registered Users");
+    this.owners = new GroupReference(
+        AccountGroup.PROJECT_OWNERS,
+        "Project Owners");
+  }
+
+  public AllProjectsCreator setAdministrators(GroupReference admin) {
+    this.admin = admin;
+    return this;
+  }
+
+  public AllProjectsCreator setBatchUsers(GroupReference batch) {
+    this.batch = batch;
+    return this;
+  }
+
+  public void create() throws IOException, ConfigInvalidException {
+    Repository git = null;
+    try {
+      git = mgr.openRepository(allProjectsName);
+      initAllProjects(git);
+    } catch (RepositoryNotFoundException notFound) {
+      // A repository may be missing if this project existed only to store
+      // inheritable permissions. For example 'All-Projects'.
+      try {
+        git = mgr.createRepository(allProjectsName);
+        initAllProjects(git);
+
+        RefUpdate u = git.updateRef(Constants.HEAD);
+        u.link(GitRepositoryManager.REF_CONFIG);
+      } catch (RepositoryNotFoundException err) {
+        String name = allProjectsName.get();
+        throw new IOException("Cannot create repository " + name, err);
+      }
+    } finally {
+      if (git != null) {
+        git.close();
+      }
+    }
+  }
+
+  private void initAllProjects(Repository git)
+      throws IOException, ConfigInvalidException {
+    MetaDataUpdate md = new MetaDataUpdate(
+        GitReferenceUpdated.DISABLED,
+        allProjectsName,
+        git);
+    md.getCommitBuilder().setAuthor(serverUser);
+    md.getCommitBuilder().setCommitter(serverUser);
+    md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+
+    ProjectConfig config = ProjectConfig.read(md);
+    Project p = config.getProject();
+    p.setDescription("Access inherited by all other projects.");
+    p.setRequireChangeID(InheritableBoolean.TRUE);
+    p.setUseContentMerge(InheritableBoolean.TRUE);
+    p.setUseContributorAgreements(InheritableBoolean.FALSE);
+    p.setUseSignedOffBy(InheritableBoolean.FALSE);
+
+    AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+    AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+    AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+    AccessSection tags = config.getAccessSection("refs/tags/*", true);
+    AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+    AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
+
+    grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
+    grant(config, all, Permission.READ, admin, anonymous);
+
+    if (batch != null) {
+      Permission priority = cap.getPermission(GlobalCapability.PRIORITY, true);
+      PermissionRule r = rule(config, batch);
+      r.setAction(Action.BATCH);
+      priority.add(r);
+
+      Permission stream = cap.getPermission(GlobalCapability.STREAM_EVENTS, true);
+      stream.add(rule(config, batch));
+    }
+
+    LabelType cr = initCodeReviewLabel(config);
+    grant(config, heads, cr, -1, 1, registered);
+    grant(config, heads, cr, -2, 2, admin, owners);
+    grant(config, heads, Permission.CREATE, admin, owners);
+    grant(config, heads, Permission.PUSH, admin, owners);
+    grant(config, heads, Permission.SUBMIT, admin, owners);
+    grant(config, heads, Permission.FORGE_AUTHOR, registered);
+    grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
+    grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
+
+    grant(config, tags, Permission.PUSH_TAG, admin, owners);
+    grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
+
+    grant(config, magic, Permission.PUSH, registered);
+    grant(config, magic, Permission.PUSH_MERGE, registered);
+
+    meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
+    grant(config, meta, Permission.READ, admin, owners);
+    grant(config, meta, cr, -2, 2, admin, owners);
+    grant(config, meta, Permission.PUSH, admin, owners);
+    grant(config, meta, Permission.SUBMIT, admin, owners);
+
+    config.commit(md);
+  }
+
+  private void grant(ProjectConfig config, AccessSection section,
+      String permission, GroupReference... groupList) {
+    grant(config, section, permission, false, groupList);
+  }
+
+  private void grant(ProjectConfig config, AccessSection section,
+      String permission, boolean force, GroupReference... groupList) {
+    Permission p = section.getPermission(permission, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setForce(force);
+        p.add(r);
+      }
+    }
+  }
+
+  private void grant(ProjectConfig config,
+      AccessSection section, LabelType type,
+      int min, int max, GroupReference... groupList) {
+    String name = Permission.LABEL + type.getName();
+    Permission p = section.getPermission(name, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setRange(min, max);
+        p.add(r);
+      }
+    }
+  }
+
+  private PermissionRule rule(ProjectConfig config, GroupReference group) {
+    return new PermissionRule(config.resolve(group));
+  }
+
+  public static LabelType initCodeReviewLabel(ProjectConfig c) {
+    LabelType type = new LabelType("Code-Review", ImmutableList.of(
+        new LabelValue((short) 2, "Looks good to me, approved"),
+        new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
+        new LabelValue((short) 0, "No score"),
+        new LabelValue((short) -1, "I would prefer that you didn't submit this"),
+        new LabelValue((short) -2, "Do not submit")));
+    type.setAbbreviatedName("CR");
+    type.setCopyMinScore(true);
+    c.getLabelSections().put(type.getName(), type);
+    return type;
+  }
+}
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 0c54883..6c9ca59d 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
@@ -39,18 +39,30 @@
 
 /** Provides access to the DataSource. */
 @Singleton
-public final class DataSourceProvider implements Provider<DataSource>,
+public class DataSourceProvider implements Provider<DataSource>,
     LifecycleListener {
-  private final DataSource ds;
+  private final SitePaths site;
+  private final Config cfg;
+  private final Context ctx;
+  private final DataSourceType dst;
+  private DataSource ds;
 
   @Inject
-  DataSourceProvider(final SitePaths site,
-      @GerritServerConfig final Config cfg, Context ctx, DataSourceType dst) {
-    ds = open(site, cfg, ctx, dst);
+  protected DataSourceProvider(SitePaths site,
+      @GerritServerConfig Config cfg,
+      Context ctx,
+      DataSourceType dst) {
+    this.site = site;
+    this.cfg = cfg;
+    this.ctx = ctx;
+    this.dst = dst;
   }
 
   @Override
   public synchronized DataSource get() {
+    if (ds == null) {
+      ds = open(site, cfg, ctx, dst);
+    }
     return ds;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
index 8c46732..308cec8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
@@ -47,6 +47,10 @@
 
   @Override
   public boolean usePool() {
+    // MySQL has given us trouble with the connection pool,
+    // sometimes the backend disconnects and the pool winds
+    // up with a stale connection. Fortunately opening up
+    // a new MySQL connection is usually very fast.
     return false;
   }
 }
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 766d673..0e89a75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,41 +14,23 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Version;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 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.account.GroupUUID;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePath;
 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.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
 
 import java.io.File;
 import java.io.IOException;
@@ -59,8 +41,7 @@
   private final @SitePath
   File site_path;
 
-  private final GitRepositoryManager mgr;
-  private final AllProjectsName allProjectsName;
+  private final AllProjectsCreator allProjectsCreator;
   private final PersonIdent serverUser;
   private final DataSourceType dataSourceType;
 
@@ -70,26 +51,24 @@
   private AccountGroup anonymous;
   private AccountGroup registered;
   private AccountGroup owners;
+  private AccountGroup batch;
 
   @Inject
   public SchemaCreator(SitePaths site,
       @Current SchemaVersion version,
-      GitRepositoryManager mgr,
-      AllProjectsName allProjectsName,
+      AllProjectsCreator ap,
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst) {
-    this(site.site_path, version, mgr, allProjectsName, au, dst);
+    this(site.site_path, version, ap, au, dst);
   }
 
   public SchemaCreator(@SitePath File site,
       @Current SchemaVersion version,
-      GitRepositoryManager gitMgr,
-      AllProjectsName ap,
+      AllProjectsCreator ap,
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst) {
     site_path = site;
-    mgr = gitMgr;
-    allProjectsName = ap;
+    allProjectsCreator = ap;
     serverUser = au;
     dataSourceType = dst;
     versionNbr = version.getVersionNbr();
@@ -110,7 +89,10 @@
     db.schemaVersion().insert(Collections.singleton(sVer));
 
     initSystemConfig(db);
-    initAllProjects();
+    allProjectsCreator
+      .setAdministrators(GroupReference.forGroup(admin))
+      .setBatchUsers(GroupReference.forGroup(batch))
+      .create();
     dataSourceType.getIndexScript().run(db);
   }
 
@@ -151,13 +133,13 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(registered)));
 
-    final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
-    batchUsers.setDescription("Users who perform batch actions on Gerrit");
-    batchUsers.setOwnerGroupUUID(admin.getGroupUUID());
-    batchUsers.setType(AccountGroup.Type.INTERNAL);
-    c.accountGroups().insert(Collections.singleton(batchUsers));
+    batch = newGroup(c, "Non-Interactive Users", null);
+    batch.setDescription("Users who perform batch actions on Gerrit");
+    batch.setOwnerGroupUUID(admin.getGroupUUID());
+    batch.setType(AccountGroup.Type.INTERNAL);
+    c.accountGroups().insert(Collections.singleton(batch));
     c.accountGroupNames().insert(
-        Collections.singleton(new AccountGroupName(batchUsers)));
+        Collections.singleton(new AccountGroupName(batch)));
 
     owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
     owners.setDescription("Any owner of the project");
@@ -176,118 +158,4 @@
     c.systemConfig().insert(Collections.singleton(s));
     return s;
   }
-
-  private void initAllProjects() throws IOException, ConfigInvalidException {
-    Repository git = null;
-    try {
-      git = mgr.openRepository(allProjectsName);
-      initAllProjects(git);
-    } catch (RepositoryNotFoundException notFound) {
-      // A repository may be missing if this project existed only to store
-      // inheritable permissions. For example 'All-Projects'.
-      try {
-        git = mgr.createRepository(allProjectsName);
-        initAllProjects(git);
-        final RefUpdate u = git.updateRef(Constants.HEAD);
-        u.link(GitRepositoryManager.REF_CONFIG);
-      } catch (RepositoryNotFoundException err) {
-        final String name = allProjectsName.get();
-        throw new IOException("Cannot create repository " + name, err);
-      }
-    } finally {
-      if (git != null) {
-        git.close();
-      }
-    }
-  }
-
-  private void initAllProjects(Repository git) throws IOException,
-      ConfigInvalidException {
-      MetaDataUpdate md =
-          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
-      md.getCommitBuilder().setAuthor(serverUser);
-      md.getCommitBuilder().setCommitter(serverUser);
-
-      ProjectConfig config = ProjectConfig.read(md);
-      Project p = config.getProject();
-      p.setDescription("Access inherited by all other projects.");
-      p.setRequireChangeID(InheritableBoolean.TRUE);
-      p.setUseContentMerge(InheritableBoolean.TRUE);
-      p.setUseContributorAgreements(InheritableBoolean.FALSE);
-      p.setUseSignedOffBy(InheritableBoolean.FALSE);
-
-      AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
-      AccessSection all = config.getAccessSection(AccessSection.ALL, true);
-      AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
-      AccessSection tags = config.getAccessSection("refs/tags/*", true);
-      AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
-      AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
-
-      grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
-      grant(config, all, Permission.READ, admin, anonymous);
-
-      LabelType cr = initCodeReviewLabel(config);
-      grant(config, heads, cr, -1, 1, registered);
-      grant(config, heads, cr, -2, 2, admin, owners);
-      grant(config, heads, Permission.CREATE, admin, owners);
-      grant(config, heads, Permission.PUSH, admin, owners);
-      grant(config, heads, Permission.SUBMIT, admin, owners);
-      grant(config, heads, Permission.FORGE_AUTHOR, registered);
-      grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
-
-      grant(config, tags, Permission.PUSH_TAG, admin, owners);
-      grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
-
-      grant(config, magic, Permission.PUSH, registered);
-      grant(config, magic, Permission.PUSH_MERGE, registered);
-
-      meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
-      grant(config, meta, Permission.READ, admin, owners);
-      grant(config, meta, cr, -2, 2, admin, owners);
-      grant(config, meta, Permission.PUSH, admin, owners);
-      grant(config, meta, Permission.SUBMIT, admin, owners);
-
-      md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
-      config.commit(md);
-  }
-
-  private PermissionRule grant(ProjectConfig config, AccessSection section,
-      String permission, AccountGroup group1, AccountGroup... groupList) {
-    Permission p = section.getPermission(permission, true);
-    PermissionRule rule = rule(config, group1);
-    p.add(rule);
-    for (AccountGroup group : groupList) {
-      p.add(rule(config, group));
-    }
-    return rule;
-  }
-
-  private void grant(ProjectConfig config,
-      AccessSection section, LabelType type,
-      int min, int max, AccountGroup... groupList) {
-    String name = Permission.LABEL + type.getName();
-    Permission p = section.getPermission(name, true);
-    for (AccountGroup group : groupList) {
-      PermissionRule r = rule(config, group);
-      r.setRange(min, max);
-      p.add(r);
-    }
-  }
-
-  private PermissionRule rule(ProjectConfig config, AccountGroup group) {
-    return new PermissionRule(config.resolve(group));
-  }
-
-  public static LabelType initCodeReviewLabel(ProjectConfig c) {
-    LabelType type = new LabelType("Code-Review", ImmutableList.of(
-        new LabelValue((short) 2, "Looks good to me, approved"),
-        new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
-        new LabelValue((short) 0, "No score"),
-        new LabelValue((short) -1, "I would prefer that you didn't submit this"),
-        new LabelValue((short) -2, "Do not submit")));
-    type.setAbbreviatedName("CR");
-    type.setCopyMinScore(true);
-    c.getLabelSections().put(type.getName(), type);
-    return type;
-  }
 }
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..ac59f65 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
@@ -147,7 +147,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_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index 2ab83c8..bf488e3 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
@@ -125,7 +125,7 @@
         }
 
         AccountGroup batch = db.accountGroups().get(sc.batchUsersGroupId);
-        stmt.setInt(0, sc.batchUsersGroupId.get());
+        stmt.setInt(1, sc.batchUsersGroupId.get());
         if (batch != null
             && db.accountGroupMembers().byGroup(sc.batchUsersGroupId).toList().isEmpty()
             &&  stmt.executeQuery().first() != false) {
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..31d1953 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
@@ -28,7 +28,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;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
new file mode 100644
index 0000000..a836fd7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.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.util;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PluginUser;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+/** RequestContext active while plugins load or unload. */
+public class PluginRequestContext implements RequestContext {
+  private final PluginUser user;
+
+  public PluginRequestContext(PluginUser user) {
+    this.user = user;
+  }
+
+  @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/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 98b0b4a..117c57b 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
@@ -80,7 +80,7 @@
       for (LabelType label : labelTypes.getLabelTypes()) {
         config.getLabelSections().put(label.getName(), label);
       }
-      allProjects = new ProjectState(this, allProjectsName, null,
+      allProjects = new ProjectState(null, this, allProjectsName, null, null,
           null, null, null, config);
     }
 
@@ -96,11 +96,21 @@
     }
 
     @Override
+    public ProjectState checkedGet(Project.NameKey projectName) {
+      return get(projectName);
+    }
+
+    @Override
     public void evict(Project p) {
       throw new UnsupportedOperationException();
     }
 
     @Override
+    public void evict(Project.NameKey p) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
     public void remove(Project p) {
       throw new UnsupportedOperationException();
     }
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/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 51ea5a2..968d661 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
@@ -420,10 +420,19 @@
       }
 
       @Override
+      public ProjectState checkedGet(Project.NameKey projectName) {
+        return get(projectName);
+      }
+
+      @Override
       public void evict(Project p) {
       }
 
       @Override
+      public void evict(Project.NameKey p) {
+      }
+
+      @Override
       public void remove(Project p) {
       }
 
@@ -540,11 +549,11 @@
     ProjectControl.AssistedFactory projectControlFactory = null;
     RulesCache rulesCache = null;
     all.put(local.getProject().getNameKey(), new ProjectState(
-        projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, local));
+        null, projectCache, allProjectsName, projectControlFactory,
+        envFactory, mgr, rulesCache, null, local));
     all.put(parent.getProject().getNameKey(), new ProjectState(
-        projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, parent));
+        null, projectCache, allProjectsName, projectControlFactory,
+        envFactory, mgr, rulesCache, null, parent));
     return all.get(local.getProject().getNameKey());
   }
 
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 d9b237b..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,16 +52,27 @@
 
 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;
 import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
 import org.junit.Before;
 
 import java.io.File;
-import java.net.URISyntaxException;
+import java.io.FileOutputStream;
+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 final Map<String, File> hooks = Maps.newTreeMap();
+  private final List<File> cleanup = Lists.newArrayList();
 
   @Override
   @Before
@@ -70,33 +81,62 @@
     repository = createWorkRepository();
   }
 
-  protected File getHook(final String name) {
+  @Override
+  @After
+  public void tearDown() throws Exception {
+    super.tearDown();
+    for (File p : cleanup) {
+      if (!p.delete()) {
+        p.deleteOnExit();
+      }
+    }
+    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;
-    final URL url = cl().getResource(path);
+    URL url = cl().getResource(path);
     if (url == null) {
       fail("Cannot locate " + path + " in CLASSPATH");
     }
 
-    File hook;
-    try {
-      hook = new File(url.toURI());
-    } catch (URISyntaxException e) {
+    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())) {
+      InputStream in = url.openStream();
+      try {
+        hook = File.createTempFile("hook_", ".sh");
+        cleanup.add(hook);
+        FileOutputStream out = new FileOutputStream(hook);
+        try {
+          ByteStreams.copy(in, out);
+        } finally {
+          out.close();
+        }
+      } finally {
+        in.close();
+      }
+      hook.setExecutable(true);
+      hooks.put(name, hook);
+      return hook;
+    } else {
+      fail("Cannot invoke " + url);
+      return null;
     }
-    if (!hook.isFile()) {
-      fail("Cannot locate " + path + " in CLASSPATH");
-    }
-
-    // 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-sshd/BUCK b/gerrit-sshd/BUCK
new file mode 100644
index 0000000..769be58
--- /dev/null
+++ b/gerrit-sshd/BUCK
@@ -0,0 +1,31 @@
+java_library2(
+  name = 'sshd',
+  srcs = glob(['src/main/java/**/*.java']),
+  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'],
+)
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 75a43c0..119873e 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
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/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 83bc8a5..8dc3f2c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -15,18 +15,14 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.apache.commons.codec.binary.Base64;
-import org.apache.mina.core.future.IoFuture;
-import org.apache.mina.core.future.IoFutureListener;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.util.Buffer;
@@ -104,7 +100,7 @@
       if (myHostKeys.contains(suppliedKey)
           || getPeerKeys().contains(suppliedKey)) {
         PeerDaemonUser user = peerFactory.create(sd.getRemoteAddress());
-        return success(username, session, sd, user);
+        return SshUtil.success(username, session, sshScope, sshLog, sd, user);
 
       } else {
         sd.authenticationError(username, "no-matching-key");
@@ -144,12 +140,14 @@
       }
     }
 
-    if (!createUser(sd, key).getAccount().isActive()) {
+    if (!SshUtil.createUser(sd, userFactory, key.getAccount())
+        .getAccount().isActive()) {
       sd.authenticationError(username, "inactive-account");
       return false;
     }
 
-    return success(username, session, sd, createUser(sd, key));
+    return SshUtil.success(username, session, sshScope, sshLog, sd,
+        SshUtil.createUser(sd, userFactory, key.getAccount()));
   }
 
   private Set<PublicKey> getPeerKeys() {
@@ -161,46 +159,6 @@
     return p.keys;
   }
 
-  private boolean success(final String username, final ServerSession session,
-      final SshSession sd, final CurrentUser user) {
-    if (sd.getCurrentUser() == null) {
-      sd.authenticationSuccess(username, user);
-
-      // If this is the first time we've authenticated this
-      // session, record a login event in the log and add
-      // a close listener to record a logout event.
-      //
-      Context ctx = sshScope.newContext(null, sd, null);
-      Context old = sshScope.set(ctx);
-      try {
-        sshLog.onLogin();
-      } finally {
-        sshScope.set(old);
-      }
-
-      session.getIoSession().getCloseFuture().addListener(
-          new IoFutureListener<IoFuture>() {
-            @Override
-            public void operationComplete(IoFuture future) {
-              final Context ctx = sshScope.newContext(null, sd, null);
-              final Context old = sshScope.set(ctx);
-              try {
-                sshLog.onLogout();
-              } finally {
-                sshScope.set(old);
-              }
-            }
-          });
-    }
-
-    return true;
-  }
-
-  private IdentifiedUser createUser(final SshSession sd,
-      final SshKeyCacheEntry key) {
-    return userFactory.create(sd.getRemoteAddress(), key.getAccount());
-  }
-
   private SshKeyCacheEntry find(final Iterable<SshKeyCacheEntry> keyList,
       final PublicKey suppliedKey) {
     for (final SshKeyCacheEntry k : keyList) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
new file mode 100644
index 0000000..17db6b9
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 Goldman Sachs
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Authenticates users with kerberos (gssapi-with-mic).
+ */
+@Singleton
+class GerritGSSAuthenticator extends GSSAuthenticator {
+  private final AccountCache accounts;
+  private final SshScope sshScope;
+  private final SshLog sshLog;
+  private final GenericFactory userFactory;
+
+  @Inject
+  GerritGSSAuthenticator(final AccountCache accounts, final SshScope sshScope,
+      final SshLog sshLog, final IdentifiedUser.GenericFactory userFactory) {
+    this.accounts = accounts;
+    this.sshScope = sshScope;
+    this.sshLog = sshLog;
+    this.userFactory = userFactory;
+  }
+
+  @Override
+  public boolean validateIdentity(final ServerSession session,
+      final String identity) {
+    final SshSession sd = session.getAttribute(SshSession.KEY);
+    int at = identity.indexOf('@');
+    String username;
+    if (at == -1) {
+      username = identity;
+    } else {
+      username = identity.substring(0, at);
+    }
+    AccountState state = accounts.getByUsername(username);
+    Account account = state == null ? null : state.getAccount();
+    boolean active = account != null && account.isActive();
+    if (active) {
+      return SshUtil.success(username, session, sshScope, sshLog, sd,
+          SshUtil.createUser(sd, userFactory, account.getId()));
+    } else {
+      return false;
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 4e6948a..8519e94 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -19,6 +19,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -75,6 +76,8 @@
 import org.apache.sshd.server.SshFile;
 import org.apache.sshd.server.UserAuth;
 import org.apache.sshd.server.auth.UserAuthPublicKey;
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
+import org.apache.sshd.server.auth.gss.UserAuthGSS;
 import org.apache.sshd.server.channel.ChannelDirectTcpip;
 import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.kex.DHG1;
@@ -85,9 +88,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.File;
 import java.io.IOException;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
+import java.net.UnknownHostException;
 import java.security.InvalidKeyException;
 import java.security.KeyPair;
 import java.security.PublicKey;
@@ -129,6 +135,7 @@
   @Inject
   SshDaemon(final CommandFactory commandFactory, final NoShell noShell,
       final PublickeyAuthenticator userAuth,
+      final GerritGSSAuthenticator kerberosAuth,
       final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
       @GerritServerConfig final Config cfg, final SshLog sshLog,
       @SshListenAddresses final List<SocketAddress> listen,
@@ -152,6 +159,18 @@
         String.valueOf(MILLISECONDS.convert(ConfigUtil.getTimeUnit(cfg, "sshd",
             null, "loginGraceTime", 120, SECONDS), SECONDS)));
 
+    long idleTimeoutSeconds = ConfigUtil.getTimeUnit(cfg, "sshd", null,
+        "idleTimeout", 0, SECONDS);
+    if (idleTimeoutSeconds == 0) {
+      // Since Apache SSHD does not allow to turn off closing idle connections,
+      // we fake it by using the highest timeout allowed by Apache SSHD, which
+      // amounts to ~24 days.
+      idleTimeoutSeconds = MILLISECONDS.toSeconds(Integer.MAX_VALUE);
+    }
+    getProperties().put(
+        IDLE_TIMEOUT,
+        String.valueOf(SECONDS.toMillis(idleTimeoutSeconds)));
+
     final int maxConnectionsPerUser =
         cfg.getInt("sshd", "maxConnectionsPerUser", 64);
     if (0 < maxConnectionsPerUser) {
@@ -159,6 +178,11 @@
           String.valueOf(maxConnectionsPerUser));
     }
 
+    final String kerberosKeytab = cfg.getString(
+        "sshd", null, "kerberosKeytab");
+    final String kerberosPrincipal = cfg.getString(
+        "sshd", null, "kerberosPrincipal");
+
     if (SecurityUtils.isBouncyCastleRegistered()) {
       initProviderBouncyCastle();
     } else {
@@ -172,7 +196,7 @@
     initFileSystemFactory();
     initSubsystems();
     initCompression();
-    initUserAuth(userAuth);
+    initUserAuth(userAuth, kerberosAuth, kerberosKeytab, kerberosPrincipal);
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
     setShellFactory(noShell);
@@ -457,10 +481,36 @@
     setSubsystemFactories(Collections.<NamedFactory<Command>> emptyList());
   }
 
-  @SuppressWarnings("unchecked")
-  private void initUserAuth(final PublickeyAuthenticator pubkey) {
-    setUserAuthFactories(Arrays
-        .<NamedFactory<UserAuth>> asList(new UserAuthPublicKey.Factory()));
+  private void initUserAuth(final PublickeyAuthenticator pubkey,
+      final GSSAuthenticator kerberosAuthenticator,
+      String kerberosKeytab, String kerberosPrincipal) {
+    List<NamedFactory<UserAuth>> authFactories = Lists.newArrayList();
+    if (kerberosKeytab != null) {
+      authFactories.add(new UserAuthGSS.Factory());
+      log.info("Enabling kerberos with keytab " + kerberosKeytab);
+      if (!new File(kerberosKeytab).canRead()) {
+        log.error("Keytab " + kerberosKeytab +
+            " does not exist or is not readable; further errors are possible");
+      }
+      kerberosAuthenticator.setKeytabFile(kerberosKeytab);
+      if (kerberosPrincipal == null) {
+        try {
+          kerberosPrincipal = "host/" +
+              InetAddress.getLocalHost().getCanonicalHostName();
+        } catch(UnknownHostException e) {
+          kerberosPrincipal = "host/localhost";
+        }
+      }
+      log.info("Using kerberos principal " + kerberosPrincipal);
+      if (!kerberosPrincipal.startsWith("host/")) {
+        log.warn("Host principal does not start with host/ " +
+            "which most SSH clients will supply automatically");
+      }
+      kerberosAuthenticator.setServicePrincipalName(kerberosPrincipal);
+      setGSSAuthenticator(kerberosAuthenticator);
+    }
+    authFactories.add(new UserAuthPublicKey.Factory());
+    setUserAuthFactories(authFactories);
     setPublickeyAuthenticator(pubkey);
   }
 
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 6fad42b..2e42c23 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
@@ -42,6 +42,7 @@
 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;
 import org.eclipse.jgit.lib.Config;
 
 import java.net.SocketAddress;
@@ -84,6 +85,7 @@
         .toProvider(StreamCommandExecutorProvider.class).in(SINGLETON);
     bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
 
+    bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
     bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
 
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 da245a3..6a4d995 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
@@ -14,12 +14,19 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.sshd.SshScope.Context;
 
 import org.apache.commons.codec.binary.Base64;
+import org.apache.mina.core.future.IoFuture;
+import org.apache.mina.core.future.IoFutureListener;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.util.Buffer;
+import org.apache.sshd.server.session.ServerSession;
 import org.eclipse.jgit.lib.Constants;
 
 import java.io.BufferedReader;
@@ -112,4 +119,46 @@
       return keyStr;
     }
   }
+
+  public static boolean success(final String username, final ServerSession session,
+      final SshScope sshScope, final SshLog sshLog,
+      final SshSession sd, final CurrentUser user) {
+    if (sd.getCurrentUser() == null) {
+      sd.authenticationSuccess(username, user);
+
+      // If this is the first time we've authenticated this
+      // session, record a login event in the log and add
+      // a close listener to record a logout event.
+      //
+      Context ctx = sshScope.newContext(null, sd, null);
+      Context old = sshScope.set(ctx);
+      try {
+        sshLog.onLogin();
+      } finally {
+        sshScope.set(old);
+      }
+
+      session.getIoSession().getCloseFuture().addListener(
+          new IoFutureListener<IoFuture>() {
+            @Override
+            public void operationComplete(IoFuture future) {
+              final Context ctx = sshScope.newContext(null, sd, null);
+              final Context old = sshScope.set(ctx);
+              try {
+                sshLog.onLogout();
+              } finally {
+                sshScope.set(old);
+              }
+            }
+          });
+    }
+
+    return true;
+  }
+
+  public static IdentifiedUser createUser(final SshSession sd,
+      final IdentifiedUser.GenericFactory userFactory,
+      final Account.Id account) {
+    return userFactory.create(sd.getRemoteAddress(), account);
+  }
 }
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 f00379b..15229dc 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
@@ -72,7 +72,7 @@
   private void checkPermission() throws PermissionDeniedException {
     if (!currentUser.getCapabilities().canAccessDatabase()) {
       throw new PermissionDeniedException(String.format(
-          "%s does not have \"Perform Raw Query\" capability.",
+          "%s does not have \"Access Database\" capability.",
           currentUser.getUserName()));
     }
   }
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..e83963a 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;
@@ -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/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..6c4b41c 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,8 +35,6 @@
 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. **/
@@ -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/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..4b78cfc 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,17 +17,16 @@
 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. */
@@ -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/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
new file mode 100644
index 0000000..c5bda3c
--- /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", descr = "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 244028c..ab70395 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
@@ -21,6 +21,8 @@
 
 import org.apache.sshd.server.Environment;
 
+import java.util.List;
+
 @CommandMetaData(name = "ls-projects", descr = "List projects visible to the caller")
 final class ListProjectsCommand extends BaseCommand {
   @Inject
@@ -33,7 +35,8 @@
       public void run() throws Exception {
         parseCommandLine(impl);
         if (!impl.getFormat().isJson()) {
-          if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+          List<String> showBranch = impl.getShowBranch();
+          if (impl.isShowTree() && (showBranch != null) && !showBranch.isEmpty()) {
             throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
           }
           if (impl.isShowTree() && impl.isShowDescription()) {
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 2be3d86..58abf95 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
@@ -58,11 +58,11 @@
   private ChangeCache changeCache;
 
   @Option(name = "--project", aliases = {"-p"}, metaVar = "PROJECT",
-      usage = "project for which the refs should be listed")
+      required = true, usage = "project for which the refs should be listed")
   private ProjectControl projectControl;
 
   @Option(name = "--user", aliases = {"-u"},  metaVar = "USER",
-      usage = "user for which the groups should be listed")
+      required = true, usage = "user for which the groups should be listed")
   private String userName;
 
   @Option(name = "--only-refs-heads", usage = "list only refs under refs/heads")
@@ -73,10 +73,6 @@
 
   @Override
   protected void run() throws Failure {
-    if (userName == null || projectControl == null) {
-      throw new UnloggedFailure(1, "fatal: --user and --project options must be used.");
-    }
-
     Account userAccount = null;
     try {
       userAccount = accountResolver.find(userName);
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 48e175d..ffbd251 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
@@ -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;
 
@@ -218,10 +214,8 @@
 
   private void applyReview(final ChangeControl ctl, final PatchSet patchSet,
       final PostReview.Input review) throws Exception {
-    if (!review.labels.isEmpty()) {
-      reviewProvider.get().apply(new RevisionResource(
-          new ChangeResource(ctl), patchSet), review);
-    }
+    reviewProvider.get().apply(new RevisionResource(
+        new ChangeResource(ctl), patchSet), review);
   }
 
   private void approveOne(final PatchSet patchSet) throws Exception {
@@ -246,7 +240,7 @@
     // If review labels are being applied, the comment will be included
     // on the review note. We don't need to add it again on the abandon
     // or restore comment.
-    if (!review.labels.isEmpty()) {
+    if (!review.labels.isEmpty() && (abandonChange || restoreChange)) {
       changeComment = null;
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 09c25ff..987380f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -92,6 +92,7 @@
 
   private void runImp() {
     try {
+      readAck();
       if (error != null) {
         throw error;
       }
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 8b8e1d2..a3e9d6e 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
@@ -147,6 +147,7 @@
     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");
       }
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 3366841..ae5dc56 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,6 +21,7 @@
 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;
@@ -208,13 +209,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;
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 1b81a47..99d4baa 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
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.git.WorkQueue;
@@ -33,6 +35,7 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.LinkedBlockingQueue;
 
+@RequiresCapability(GlobalCapability.STREAM_EVENTS)
 @CommandMetaData(name = "stream-events", descr = "Monitor events occurring in real time")
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
diff --git a/gerrit-util-cli/BUCK b/gerrit-util-cli/BUCK
new file mode 100644
index 0000000..c281e86
--- /dev/null
+++ b/gerrit-util-cli/BUCK
@@ -0,0 +1,11 @@
+java_library(
+  name = 'cli',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//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
index f2325a4..3dcfee2 100644
--- a/gerrit-util-cli/pom.xml
+++ b/gerrit-util-cli/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
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..5f6130c 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,6 +34,7 @@
 
 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;
@@ -153,12 +154,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 +182,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;
   }
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
index 1e941e0..14d8c8b 100644
--- a/gerrit-util-ssl/pom.xml
+++ b/gerrit-util-ssl/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
new file mode 100644
index 0000000..8cf24ad
--- /dev/null
+++ b/gerrit-war/BUCK
@@ -0,0 +1,51 @@
+java_library2(
+  name = 'init',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-extension-api:api',
+    '//gerrit-httpd:httpd',
+    '//gerrit-openid:openid',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:common_rules',
+    '//gerrit-server:server',
+    '//gerrit-sshd:sshd',
+    '//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 $(dirname $SRCS) .',
+  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',
+  ],
+)
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index dc1a3f7..afe4996 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.7-SNAPSHOT</version>
+    <version>2.8-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
@@ -120,6 +120,38 @@
     </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>
@@ -194,6 +226,19 @@
               <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>
 
@@ -242,7 +287,7 @@
             <id>include-documentation</id>
             <phase>process-classes</phase>
             <configuration>
-              <target if="gerrit.include-documentation">
+              <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" />
@@ -269,7 +314,7 @@
             <id>include-release-notes</id>
             <phase>process-classes</phase>
             <configuration>
-              <target if="gerrit.include-documentation">
+              <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" />
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index 250fa4c..1fcca6d 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -38,10 +38,8 @@
 
 # Silence non-critical messages from openid4java
 #
+log4j.logger.org.apache.http=WARN
 log4j.logger.org.apache.xml=WARN
-log4j.logger.httpclient.wire=WARN
-log4j.logger.org.apache.commons.httpclient=WARN
-log4j.logger.org.apache.commons.httpclient.HttpMethodBase=ERROR
 log4j.logger.org.openid4java=WARN
 log4j.logger.org.openid4java.consumer.ConsumerManager=FATAL
 log4j.logger.org.openid4java.discovery.Discovery=ERROR
diff --git a/lib/BUCK b/lib/BUCK
new file mode 100644
index 0000000..9f24e6f
--- /dev/null
+++ b/lib/BUCK
@@ -0,0 +1,247 @@
+include_defs('//lib/maven.defs')
+
+define_license(name = 'Apache1.1')
+define_license(name = 'Apache2.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 = 'h2')
+define_license(name = 'jgit')
+define_license(name = 'jsch')
+define_license(name = 'jsr305')
+define_license(name = 'ow2')
+define_license(name = 'postgresql')
+define_license(name = 'prologcafe')
+define_license(name = 'slf4j')
+define_license(name = 'DO_NOT_DISTRIBUTE')
+
+maven_jar(
+  name = 'gwtorm',
+  id = 'gwtorm:gwtorm:1.6',
+  bin_sha1 = '61bcb92f438524260429149910b5261d48812419',
+  src_sha1 = '2624f9d6a750a8aa8f9a5d4b5062b70cd12d1ae7',
+  license = 'Apache2.0',
+  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:14.0',
+  sha1 = '67b7be4ee7ba48e4828a42d6d5069761186d4a53',
+  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.44-1',
+  sha1 = '2e9ae08de5a71bd0e0d3ba2558598181bfa71d4e',
+  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 = 'jsr305',
+  id = 'com.google.code.findbugs:jsr305:1.3.9',
+  sha1 = '40719ea6961c0cb6afaeb6a921eaa1f6afd4cfdf',
+  license = 'jsr305',
+  attach_source = False,
+  exclude_java_sources = True,
+)
+
+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.168',
+  sha1 = 'eb32936a239d95220f5b2d2973a7b17372f98b54',
+  license = 'h2',
+)
+
+maven_jar(
+  name = 'postgresql',
+  id = 'postgresql:postgresql:9.1-901-1.jdbc4',
+  sha1 = '9bfabe48876ec38f6cbaa6931bad05c64a9ea942',
+  license = 'postgresql',
+  attach_source = False,
+)
+
+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/DEFS b/lib/DEFS
new file mode 100644
index 0000000..af90a8a
--- /dev/null
+++ b/lib/DEFS
@@ -0,0 +1,158 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 genantlr(
+    name,
+    srcs,
+    outs):
+  genrule(
+    name = name,
+    srcs = srcs,
+    cmd = '${//lib/antlr:antlr-tool} -o $(dirname $OUT) $SRCS',
+    deps = ['//lib/antlr:antlr-tool'],
+    out = outs[0],
+  )
+
+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 = ['${//lib/gwt:compiler}', module_target, '$OUT']
+  cmd += compiler_opts + ['--', '$DEPS']
+  genrule(
+    name = name,
+    srcs = [],
+    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,
+    visibility = ['PUBLIC']):
+  gerrit_plugin(
+    name = name,
+    deps = deps,
+    srcs = srcs,
+    resources = resources,
+    manifest_file = manifest_file,
+    type = 'extension',
+    visibility = visibility,
+  )
+
+def gerrit_plugin(
+    name,
+    deps = [],
+    srcs = [],
+    resources = [],
+    manifest_file = None,
+    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'
+  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,
+  )
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-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-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-jsr305 b/lib/LICENSE-jsr305
new file mode 100644
index 0000000..cf30ea2
--- /dev/null
+++ b/lib/LICENSE-jsr305
@@ -0,0 +1,28 @@
+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.
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-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/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/commons/BUCK b/lib/commons/BUCK
new file mode 100644
index 0000000..e5c78ab
--- /dev/null
+++ b/lib/commons/BUCK
@@ -0,0 +1,79 @@
+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'],
+)
+
+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 = 'logging',
+  id = 'commons-logging:commons-logging:1.1.1',
+  sha1 = '5043bfebc3db072ed80fbd362e7caf00e885d8ae',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE',
+    'META-INF/NOTICE',
+  ],
+  attach_source = False,
+  visibility = ['//lib/openid:'],
+)
+
+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'],
+)
diff --git a/lib/guice/BUCK b/lib/guice/BUCK
new file mode 100644
index 0000000..48c3be3
--- /dev/null
+++ b/lib/guice/BUCK
@@ -0,0 +1,62 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '3.0'
+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 = '9d84f15fe35e2c716a02979fb62f50a29f38aefa',
+  license = 'Apache2.0',
+  deps = [':aopalliance'],
+  exclude = EXCLUDE,
+  visibility = [],
+)
+
+maven_jar(
+  name = 'guice-assistedinject',
+  id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION,
+  sha1 = '544449ddb19f088dcde44f055d30a08835a954a7',
+  license = 'Apache2.0',
+  deps = [':guice'],
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'guice-servlet',
+  id = 'com.google.inject.extensions:guice-servlet:' + VERSION,
+  sha1 = '610cde0e8da5a8b7d8efb8f0b8987466ffebaaf9',
+  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..2204422
--- /dev/null
+++ b/lib/gwt/BUCK
@@ -0,0 +1,38 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '2.5.0'
+
+maven_jar(
+  name = 'user',
+  id = 'com.google.gwt:gwt-user:' + VERSION,
+  sha1 = 'bbec0cc2ba81a2601aac55e3783ad0d0ff7d45ac',
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'dev',
+  id = 'com.google.gwt:gwt-dev:' + VERSION,
+  sha1 = '3c227524e0978036e76e73e125ac8535a9e8fd7c',
+  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'],
+)
diff --git a/lib/gwt/compiler.py b/lib/gwt/compiler.py
new file mode 100755
index 0000000..4318aac
--- /dev/null
+++ b/lib/gwt/compiler.py
@@ -0,0 +1,62 @@
+#!/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 environ, makedirs, mkdir, path
+from subprocess import Popen, PIPE
+from sys import argv, stderr
+
+cp, opt, end, TMP = [], [], False, environ['TMP']
+module, outzip = argv[1], argv[2]
+
+for a in argv[3:]:
+  if end:
+    if a.endswith('.jar'):
+      cp.append(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)
+
+rebuild = outzip[:-4] + '.rebuild'
+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]
+
+gwt = Popen(cmd, stdout = PIPE, stderr = PIPE)
+out, err = gwt.communicate()
+if gwt.returncode != 0:
+  print(out + err, file=stderr)
+  exit(gwt.returncode)
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..eb44417
--- /dev/null
+++ b/lib/jgit/BUCK
@@ -0,0 +1,65 @@
+include_defs('//lib/maven.defs')
+
+REPO = GERRIT
+VERS = '2.3.1.201302201838-r.211-g36144e1'
+
+maven_jar(
+  name = 'jgit',
+  id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
+  bin_sha1 = '4328d64fb5f5a5a07795965801850a10901b0979',
+  src_sha1 = 'e55f1e231138df01edee9014df94ca3c6cbf0d8e',
+  license = 'jgit',
+  repository = REPO,
+  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 = '269b4096780247a26368985b05e4b66f16785946',
+  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 = '0fa01088b0f7a847dabafec59318221828832b36',
+  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/log/BUCK b/lib/log/BUCK
new file mode 100644
index 0000000..5b541c4
--- /dev/null
+++ b/lib/log/BUCK
@@ -0,0 +1,24 @@
+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'],
+)
diff --git a/lib/maven.defs b/lib/maven.defs
new file mode 100644
index 0000000..3aeb891
--- /dev/null
+++ b/lib/maven.defs
@@ -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.
+
+GERRIT = 'GERRIT:'
+MAVEN_CENTRAL = 'MAVEN_CENTRAL:'
+
+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,
+    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
+
+  jar = path.join(name, artifact.lower() + '-' + version)
+  url = '/'.join([
+    repository,
+    group.replace('.', '/'), artifact, version,
+    artifact + '-' + version])
+
+  binjar = jar + '.jar'
+  binurl = url + '.jar'
+
+  srcjar = jar + '-src.jar'
+  srcurl = url + '-sources.jar'
+
+  cmd = ['${//tools:download_jar}', '-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')
+
+  genrule(
+    name = name + '__download_bin',
+    cmd = ' '.join(cmd),
+    srcs = [],
+    deps = ['//tools:download_jar'],
+    out = binjar,
+  )
+  license = ['//lib:LICENSE-' + license]
+
+  if src_sha1 or attach_source:
+    cmd = ['${//tools:download_jar}', '-o', '$OUT', '-u', srcurl]
+    if src_sha1:
+      cmd.extend(['-v', src_sha1])
+    genrule(
+      name = name + '__download_src',
+      cmd = ' '.join(cmd),
+      srcs = [],
+      deps = ['//tools:download_jar'],
+      out = srcjar,
+    )
+    if src_sha1:
+      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',
+      srcs = [],
+      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..3e9558a
--- /dev/null
+++ b/lib/mina/BUCK
@@ -0,0 +1,24 @@
+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.5',
+  sha1 = '0e134a3761833a3c28c79331e806f64f985a9eec',
+  license = 'Apache2.0',
+  exclude = EXCLUDE,
+)
+
+maven_jar(
+  name = 'sshd',
+  id = 'org.apache.sshd:sshd-core:0.6.0',
+  sha1 = '2b9a119dd77a1decec78b0c511ba400c8655e96e',
+  license = 'Apache2.0',
+  deps = [':core'],
+  exclude = EXCLUDE,
+)
diff --git a/lib/openid/BUCK b/lib/openid/BUCK
new file mode 100644
index 0000000..b766532
--- /dev/null
+++ b/lib/openid/BUCK
@@ -0,0 +1,63 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'consumer',
+  id = 'org.openid4java:openid4java:0.9.8',
+  sha1 = 'de4f1b33d3b0f0b2ab1d32834ec1190b39db4160',
+  license = 'Apache2.0',
+  deps = [
+    ':httpclient',
+    ':nekohtml',
+    ':xerces',
+    '//lib/commons:logging',
+    '//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 = [],
+)
+
+maven_jar(
+  name = 'httpclient',
+  id = 'org.apache.httpcomponents:httpclient:4.1',
+  sha1 = '93cd011acb220de08b57d96106e5800d7097742b',
+  license = 'Apache2.0',
+  deps = [
+    ':httpcore',
+    '//lib/commons:codec',
+    '//lib/commons:logging',
+  ],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+  visibility = ['//gerrit-acceptance-tests:'],
+)
+
+maven_jar(
+  name = 'httpcore',
+  id = 'org.apache.httpcomponents:httpcore:4.1',
+  sha1 = '33fc26c02f8043ab0ede19eadc8c9885386b255c',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
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/DEFS b/lib/prolog/DEFS
new file mode 100644
index 0000000..38d7fe5
--- /dev/null
+++ b/lib/prolog/DEFS
@@ -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.
+
+def prolog_cafe_library(
+    name,
+    srcs,
+    deps = [],
+    visibility = []):
+  genrule(
+    name = name + '_prolog2java',
+    cmd = '${//lib/prolog:compiler} $SRCS $DEPS $OUT',
+    srcs = srcs,
+    deps = [
+      '//lib/prolog:compiler',
+      '//lib/prolog:prolog-cafe',
+    ] + deps,
+    out = name + '.jar',
+  )
+  prebuilt_jar(
+    name = name,
+    binary_jar = genfile(name + '.jar'),
+    deps = [
+      ':' + name + '_prolog2java',
+      '//lib/prolog:prolog-cafe',
+    ] + deps,
+    visibility = visibility,
+  )
diff --git a/lib/prolog/java/BuckPrologCompiler.java b/lib/prolog/java/BuckPrologCompiler.java
new file mode 100644
index 0000000..b731ea7
--- /dev/null
+++ b/lib/prolog/java/BuckPrologCompiler.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.
+
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+
+public class BuckPrologCompiler {
+  public static void main(String[] argv) throws IOException, CompileException {
+    List<File> srcs = new ArrayList<File>();
+    List<File> jars = new ArrayList<File>();
+    for (int i = 0; i < argv.length - 1; i++) {
+      String s = argv[i];
+      if (s.endsWith(".pl")) {
+        srcs.add(new File(s));
+      } else if (s.endsWith(".jar")) {
+        jars.add(new File(s));
+      }
+    }
+
+    File out = new File(argv[argv.length - 1]);
+    File java = tmpdir("java");
+    File classes = tmpdir("classes");
+    for (File src : srcs) {
+      new Compiler().prologToJavaSource(src.getPath(), java.getPath());
+    }
+    javac(jars, java, classes);
+    jar(out, classes);
+  }
+
+  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 javac(List<File> cp, File java, File classes)
+      throws IOException, CompileException {
+    JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
+    if (javac == null) {
+      throw new CompileException("JDK required (running inside of JRE)");
+    }
+
+    DiagnosticCollector<JavaFileObject> d =
+        new DiagnosticCollector<JavaFileObject>();
+    StandardJavaFileManager fm = javac.getStandardFileManager(d, null, null);
+    try {
+      StringBuilder classpath = new StringBuilder();
+      for (File jar : cp) {
+        if (classpath.length() > 0) {
+          classpath.append(File.pathSeparatorChar);
+        }
+        classpath.append(jar.getPath());
+      }
+      ArrayList<String> args = new ArrayList<String>();
+      args.addAll(Arrays.asList(new String[]{
+          "-source", "6",
+          "-target", "6",
+          "-g:none",
+          "-nowarn",
+          "-d", classes.getPath()}));
+      if (classpath.length() > 0) {
+        args.add("-classpath");
+        args.add(classpath.toString());
+      }
+      if (!javac.getTask(null, fm, d, args, null,
+          fm.getJavaFileObjectsFromFiles(find(java, ".java"))).call()) {
+        StringBuilder msg = new StringBuilder();
+        for (Diagnostic<? extends JavaFileObject> err : d.getDiagnostics()) {
+          msg.append('\n').append(err.getKind()).append(": ");
+          if (err.getSource() != null) {
+            msg.append(err.getSource().getName());
+          }
+          msg.append(':').append(err.getLineNumber()).append(": ");
+          msg.append(err.getMessage(Locale.getDefault()));
+        }
+        throw new CompileException(msg.toString());
+      }
+    } finally {
+      fm.close();
+    }
+  }
+
+  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 {
+        out.setLevel(9);
+        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();
+      }
+    }
+  }
+
+  private static List<File> find(File dir, String extension) {
+    ArrayList<File> list = new ArrayList<File>();
+    for (File f : dir.listFiles()) {
+      if (f.getName().endsWith(extension)) {
+        list.add(f);
+      } else if (f.isDirectory()) {
+        list.addAll(find(f, extension));
+      }
+    }
+    return list;
+  }
+}
diff --git a/plugins/BUCK b/plugins/BUCK
new file mode 100644
index 0000000..ded6ba2
--- /dev/null
+++ b/plugins/BUCK
@@ -0,0 +1,36 @@
+BASE = get_base_path()
+CORE = [
+  'commit-message-length-validator',
+  '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/README b/plugins/README
new file mode 100644
index 0000000..00df3c5
--- /dev/null
+++ b/plugins/README
@@ -0,0 +1,11 @@
+If you are adding a directory here:
+
+- Search all pom.xml files for "CORE PLUGIN LIST".
+- Add the new plugin to that location.
+- (optional) Thank the Maven developers for making this easy.
+
+- Ensure the plugin's pom.xml <version> is the same as Gerrit's
+  own pom.xml(s). Gerrit will only embed a plugin that has the
+  same version as itself.
+
+- Register the plugin as a submodule with git submodule.
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
new file mode 160000
index 0000000..c0389bb
--- /dev/null
+++ b/plugins/commit-message-length-validator
@@ -0,0 +1 @@
+Subproject commit c0389bbf4441d290c84c0a308e710e4f21a67cd8
diff --git a/plugins/replication b/plugins/replication
new file mode 160000
index 0000000..6b5ca01
--- /dev/null
+++ b/plugins/replication
@@ -0,0 +1 @@
+Subproject commit 6b5ca0107992973f5d8b3b9b35bd68653d8c2219
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
new file mode 160000
index 0000000..446f134
--- /dev/null
+++ b/plugins/reviewnotes
@@ -0,0 +1 @@
+Subproject commit 446f13498e109c4185f6f1e3f6e44351ded3e059
diff --git a/pom.xml b/pom.xml
index 6e4c0f6..0037bdad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.7-SNAPSHOT</version>
+  <version>2.8-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -46,10 +46,9 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>2.3.1.201302201838-r.78-g8fcde4b</jgitVersion>
+    <jgitVersion>2.3.1.201302201838-r.211-g36144e1</jgitVersion>
     <gwtormVersion>1.6</gwtormVersion>
     <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
-    <gwtexpuiVersion>1.3.2</gwtexpuiVersion>
     <gwtVersion>2.5.0</gwtVersion>
     <bouncyCastleVersion>140</bouncyCastleVersion>
     <slf4jVersion>1.6.1</slf4jVersion>
@@ -77,6 +76,8 @@
     <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>
@@ -88,29 +89,29 @@
     <module>gerrit-gwtdebug</module>
     <module>gerrit-war</module>
 
-    <module>gerrit-extension-api</module>
-
-    <module>gerrit-gwtui</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>all</id>
-      <modules>
-        <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>
-    </profile>
-    <profile>
+      <id>plugins</id>
       <activation>
-        <activeByDefault>true</activeByDefault>
+        <property>
+          <name>!gerrit.plugins.skip</name>
+        </property>
       </activation>
-      <id>no-plugins</id>
+      <modules>
+        <!-- CORE PLUGIN LIST -->
+        <module>plugins/commit-message-length-validator</module>
+        <module>plugins/replication</module>
+        <module>plugins/reviewnotes</module>
+      </modules>
     </profile>
   </profiles>
 
@@ -421,7 +422,10 @@
                     </goals>
                   </pluginExecutionFilter>
                   <action>
-                    <ignore/>
+                     <execute>
+                      <runOnIncremental>false</runOnIncremental>
+                      <runOnConfiguration>true</runOnConfiguration>
+                    </execute>
                   </action>
                 </pluginExecution>
                 <pluginExecution>
@@ -434,7 +438,10 @@
                     </goals>
                   </pluginExecutionFilter>
                   <action>
-                    <ignore/>
+                    <execute>
+                      <runOnIncremental>false</runOnIncremental>
+                      <runOnConfiguration>true</runOnConfiguration>
+                   </execute>
                   </action>
                 </pluginExecution>
               </pluginExecutions>
@@ -518,22 +525,9 @@
       </dependency>
 
       <dependency>
-        <groupId>gwtexpui</groupId>
-        <artifactId>gwtexpui</artifactId>
-        <version>${gwtexpuiVersion}</version>
-      </dependency>
-      <dependency>
-        <groupId>gwtexpui</groupId>
-        <artifactId>gwtexpui</artifactId>
-        <version>${gwtexpuiVersion}</version>
-        <classifier>sources</classifier>
-      </dependency>
-
-      <dependency>
         <groupId>org.openid4java</groupId>
-        <artifactId>openid4java-consumer</artifactId>
-        <version>0.9.6</version>
-        <type>pom</type>
+        <artifactId>openid4java</artifactId>
+        <version>0.9.8</version>
         <exclusions>
           <exclusion>
             <!-- conflicts with our use of guice 3.0 -->
@@ -576,13 +570,7 @@
       <dependency>
         <groupId>org.apache.sshd</groupId>
         <artifactId>sshd-core</artifactId>
-        <version>0.5.1-r1095809</version>
-      </dependency>
-
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpclient</artifactId>
-        <version>4.0</version>
+        <version>0.6.0</version>
       </dependency>
 
       <dependency>
@@ -893,6 +881,13 @@
     </dependencies>
   </dependencyManagement>
 
+  <pluginRepositories>
+    <pluginRepository>
+      <id>gerrit-maven</id>
+      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
+    </pluginRepository>
+  </pluginRepositories>
+
   <repositories>
     <repository>
       <id>gerrit-maven</id>
@@ -903,20 +898,5 @@
       <id>jgit-repository</id>
       <url>http://download.eclipse.org/jgit/maven</url>
     </repository>
-
-    <repository>
-      <id>java.net-repository</id>
-      <url>http://download.java.net/maven/2/</url>
-    </repository>
-
-    <repository>
-      <id>clojars-repo</id>
-      <url>http://clojars.org/repo</url>
-    </repository>
-
-    <repository>
-      <id>scala-tools</id>
-      <url>http://scala-tools.org/repo-releases</url>
-    </repository>
   </repositories>
 </project>
diff --git a/tools/BUCK b/tools/BUCK
new file mode 100644
index 0000000..ee384a1
--- /dev/null
+++ b/tools/BUCK
@@ -0,0 +1,35 @@
+python_binary(
+  name = 'download_all',
+  main = 'download_all.py',
+  visibility = ['PUBLIC'],
+)
+
+python_binary(
+  name = 'download_jar',
+  main = 'download_jar.py',
+  visibility = ['PUBLIC'],
+)
+
+python_binary(
+  name = 'pack_war',
+  main = 'pack_war.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())),
+  srcs = [],
+  deps = [],
+  out = 'buck.properties',
+  visibility = ['PUBLIC'],
+)
diff --git a/tools/build.defs b/tools/build.defs
new file mode 100644
index 0000000..42cb630
--- /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.
+
+DOCS = ['//Documentation:html.zip']
+LIBS = [
+  '//gerrit-common:version',
+  '//gerrit-war:init',
+  '//gerrit-war:log4j-config',
+  '//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 = ['${//tools:pack_war}', '-o', '$OUT']
+  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,
+    pgmlibs = PGMLIBS,
+    context = [
+      '//gerrit-main:main_bin.jar',
+      '//gerrit-war:webapp_assets.zip',
+      '//gerrit-gwtexpui:clippy_swf.zip',
+      '//gerrit-gwtui:' + ui + '.zip',
+    ] + context,
+  )
diff --git a/tools/download_all.py b/tools/download_all.py
new file mode 100755
index 0000000..b21140b
--- /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'"(//.*?__download_[^"]*)" -> "//tools:download_jar"')
+
+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_jar.py b/tools/download_jar.py
new file mode 100755
index 0000000..97ee608
--- /dev/null
+++ b/tools/download_jar.py
@@ -0,0 +1,173 @@
+#!/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, symlink
+import shutil
+from subprocess import check_call, CalledProcessError
+from sys import stderr
+from zipfile import ZipFile, BadZipfile, LargeZipFile
+
+REPO_ROOTS = {
+  'GERRIT': 'http://gerrit-maven.commondatastorage.googleapis.com',
+  'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
+}
+
+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')
+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))
+    print('Download %s' % src_url, file=stderr)
+    check_call(['curl', '--proxy-anyauth', '-sfo', cache_ent, src_url])
+  except OSError as err:
+    print('error creating directory %s: %s' %
+          (path.dirname(cache_ent), 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:
+    o = cache_ent[len(root_dir) + 1:]
+    print((
+      '%s:\n' +
+      'expected %s\n' +
+      'received %s\n' +
+      '         %s\n') % (src_url, args.v, have, o), 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)
+
+safe_mkdirs(path.dirname(args.o))
+if exclude:
+  shutil.copyfile(cache_ent, args.o)
+  try:
+    check_call(['zip', '-d', args.o] + exclude)
+  except CalledProcessError as err:
+    print('error removing files from zip: %s' % err, file=stderr)
+else:
+  try:
+    link(cache_ent, args.o)
+  except OSError as err:
+    symlink(cache_ent, args.o)
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
new file mode 100644
index 0000000..f8ed0a0
--- /dev/null
+++ b/tools/eclipse/BUCK
@@ -0,0 +1,75 @@
+include_defs('//tools/build.defs')
+
+genrule(
+  name = 'eclipse',
+  cmd = '',
+  srcs = [],
+  deps = [
+    ':_classpath',
+    ':_project',
+    '//tools:buck.properties',
+  ],
+  out = '__fake.eclipse__',
+)
+
+genrule(
+  name = 'eclipse_project',
+  cmd = '',
+  srcs = [],
+  deps = [
+    ':_classpath_nocompile',
+    ':_project',
+    '//tools:buck.properties',
+  ],
+  out = '__fake.eclipse__',
+)
+
+java_library(
+  name = 'classpath',
+  deps = LIBS + PGMLIBS + [
+    '//gerrit-acceptance-tests:acceptance_tests',
+    '//gerrit-gwtdebug:gwtdebug',
+    '//gerrit-gwtui:ui_module',
+    '//gerrit-httpd:httpd_tests',
+    '//gerrit-main:main_lib',
+    '//gerrit-server:server__compile',
+    '//lib/prolog:compiler_lib',
+  ] + scan_plugins(),
+)
+
+genrule(
+  name = '_project',
+  cmd = '${:gen_project} $OUT',
+  srcs = [],
+  deps = [':gen_project'],
+  out = 'project',
+)
+
+genrule(
+  name = '_classpath',
+  cmd = '${:gen_classpath} $OUT $DEPS',
+  srcs = [],
+  deps = [
+    ':classpath',
+    ':gen_classpath',
+  ],
+  out = 'classpath',
+)
+
+genrule(
+  name = '_classpath_nocompile',
+  cmd = '${:gen_classpath}',
+  srcs = [],
+  deps = [':gen_classpath'],
+  out = '__fake.eclipse__',
+)
+
+python_binary(
+  name = 'gen_classpath',
+  main = 'gen_classpath.py',
+)
+
+python_binary(
+  name = 'gen_project',
+  main = 'gen_project.py',
+)
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/gen_classpath.py b/tools/eclipse/gen_classpath.py
new file mode 100755
index 0000000..3b5f09d
--- /dev/null
+++ b/tools/eclipse/gen_classpath.py
@@ -0,0 +1,121 @@
+#!/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 os import path, symlink
+import re
+from subprocess import Popen, PIPE
+from sys import argv
+from xml.dom import minidom
+
+OUT = argv[1] if len(argv) >= 2 else None
+ROOT = path.abspath(__file__)
+for _ in range(0, 3):
+  ROOT = path.dirname(ROOT)
+
+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',
+])
+
+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)
+
+doc = make_classpath()
+src = set()
+lib = set()
+gwt_src = set()
+gwt_lib = set()
+
+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)
+
+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 type in ['java', 'resources']:
+      p = path.join(s, 'src', env, type)
+      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')
+if OUT:
+  symlink(p, OUT)
diff --git a/tools/eclipse/gen_project.py b/tools/eclipse/gen_project.py
new file mode 100755
index 0000000..53593c0
--- /dev/null
+++ b/tools/eclipse/gen_project.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.
+#
+# TODO(sop): Remove hack after Buck supports Eclipse
+
+from __future__ import print_function
+
+from os import path, symlink
+from sys import argv
+
+OUT = argv[1]
+ROOT = path.abspath(__file__)
+for _ in range(0, 3):
+  ROOT = path.dirname(ROOT)
+
+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)
+symlink(p, OUT)
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index f007da4..8a873be 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -25,7 +25,7 @@
 <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;/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;/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;"/>
diff --git a/tools/pack_war.py b/tools/pack_war.py
new file mode 100755
index 0000000..1c14bc8
--- /dev/null
+++ b/tools/pack_war.py
@@ -0,0 +1,53 @@
+#!/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
+from os import environ, makedirs, path, symlink
+from subprocess import check_call
+try:
+  from subprocess import check_output
+except ImportError:
+  from subprocess import Popen, PIPE
+  def check_output(*cmd):
+    return Popen(*cmd, stdout=PIPE).communicate()[0]
+
+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')
+args, ctx = opts.parse_args()
+
+war = environ['TMP']
+root = war[:war.index('buck-out')]
+jars = set()
+
+def link_jars(libs, dir):
+  makedirs(dir)
+  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(dir, 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'))
+for s in ctx:
+  check_call(['unzip', '-q', '-d', war, s])
+check_call(['zip', '-9qr', args.o, '.'], cwd = war)
diff --git a/tools/release.sh b/tools/release.sh
index 7d4c457..88e4a00 100755
--- a/tools/release.sh
+++ b/tools/release.sh
@@ -1,16 +1,25 @@
 #!/bin/sh
 
-include_docs=-Dgerrit.include-documentation=1
+flags=
 
 while [ $# -gt 0 ]
 do
 	case "$1" in
 	--no-documentation|--without-documentation)
-		include_docs=
+		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.tests.skip=true"
 		shift
 		;;
 	*)
-		echo >&2 "usage: $0 [--without-documentation]"
+		echo >&2 "usage: $0 [--no-documentation] [--no-plugins] [--no-tests]"
 		exit 1
 	esac
 done
@@ -24,19 +33,8 @@
 	exit 1
 fi
 
-
-if test -n "$include_docs"
-then
-	BINARY=asciidoc
-	if ! command -v $BINARY >/dev/null 2>&1
-	then
-		echo >&2 "error: $BINARY executable was not found. Either install $BINARY or use the --without-documentation option"
-		exit 1
-	fi
-fi
-
 ./tools/version.sh --release &&
-mvn clean install $include_docs -P all
+mvn clean package verify $flags
 rc=$?
 ./tools/version.sh --reset
 
diff --git a/tools/version.sh b/tools/version.sh
index a620d9e..def099d 100755
--- a/tools/version.sh
+++ b/tools/version.sh
@@ -6,7 +6,15 @@
 # Java based Maven plugin so its fully portable.
 #
 
-POM_FILES=$(git ls-files | grep pom.xml | grep -v /src/main/resources/archetype-resources/pom.xml)
+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=*)
@@ -27,7 +35,11 @@
 	;;
 
 --reset)
-	git checkout HEAD -- $POM_FILES
+	git checkout HEAD -- $SERVER_POMS
+	for p in $PLUGINS
+	do
+		(cd plugins/$p; git checkout $(git ls-files | grep pom.xml))
+	done
 	exit $?
 	;;
 
@@ -40,7 +52,7 @@
 v*) V=$(echo "$V" | perl -pe s/^v//) ;;
 esac
 
-perl -pi -e '
+perl -pi.bak -e '
 	if ($ARGV ne $old_argv) {
 		$seen_version = 0;
 		$old_argv = $ARGV;
@@ -50,3 +62,8 @@
 		s{(<version>).*(</version>)}{${1}'"$V"'${2}};
 	}
 	' $POM_FILES
+
+for pom in $POM_FILES
+do
+	rm -f ${pom}.bak
+done