Merge "Fix preload of gr-app.js resource"
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index fd57ff7..172bb97 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -113,290 +113,4 @@
 org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
 org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
 org.eclipse.jdt.core.compiler.processAnnotations=enabled
-org.eclipse.jdt.core.compiler.source=1.8
-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_lambda_body=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
-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_member=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
-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_lambda_arrow=insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
-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_lambda_arrow=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
-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
-org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
+org.eclipse.jdt.core.compiler.source=1.8
\ No newline at end of file
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
index d990610..3d5f5f6 100644
--- a/.settings/org.eclipse.jdt.ui.prefs
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -1,60 +1,5 @@
 eclipse.preferences.version=1
-editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
-formatter_profile=_Google Format
-formatter_settings_version=12
 org.eclipse.jdt.ui.ignorelowercasenames=true
-org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax;
 org.eclipse.jdt.ui.ondemandthreshold=99
 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/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index c1cd0e8..4ef2a35 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -214,6 +214,18 @@
   bazel test --test_env=GERRIT_USE_SSH=NO //...
 ----
 
+To exclude tests that have been marked as flaky:
+
+----
+  bazel test --test_tag_filters=-flaky //...
+----
+
+To ignore cached test results:
+
+----
+  bazel test --cache_test_results=NO //...
+----
+
 == Dependencies
 
 Dependency JARs are normally downloaded as needed, but you can
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 4e168bd..435e316 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -144,7 +144,7 @@
 link:https://gerrit-review.googlesource.com/#/settings/http-password[HTTP
 Password tab of the user settings page].
 
-
+[[style]]
 === Style
 
 Gerrit generally follows the
@@ -153,7 +153,7 @@
 
 To format source code, Gerrit uses the
 link:https://github.com/google/google-java-format[`google-java-format`]
-tool (version 1.2), which automatically formats code to follow the
+tool (version 1.3), which automatically formats code to follow the
 style guide. Using this tool streamlines code review by reducing the
 need for time-consuming, tedious, and contentious discussions about
 trivial issues like whitespace.
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 5ada1e2..39fa333 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -45,12 +45,16 @@
 [[Formatting]]
 == Code Formatter Settings
 
-Import `tools/GoogleFormat.xml` using Window -> Preferences ->
-Java -> Code Style -> Formatter -> Import...
-
-This will define the 'Google Format' profile, which the project
-settings prefer when formatting source code.
-
+To format source code, Gerrit uses the
+link:https://github.com/google/google-java-format[`google-java-format`]
+tool (version 1.3), which automatically formats code to follow the
+style guide. See link:dev-contributing.html#style[Code Style] for the
+instruction how to set up command line tool that uses this formatter.
+The Eclipse plugin is provided that allows to format with the same
+formatter from within the Eclipse IDE. See
+link:https://github.com/google/google-java-format#eclipse[Eclipse plugin]
+for details how to install it. It's important to use the same plugin version
+as the `google-java-format` script.
 
 == Site Initialization
 
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 4803d83..5bd8d46 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -271,12 +271,14 @@
 
 ** [[delete]]`Delete Change` / `Delete Revision`:
 +
-Deletes the draft change / the currently viewed draft patch set.
+Deletes the change / the currently viewed draft patch set.
 +
-The `Delete Change` / `Delete Revision` buttons are only available if a
-draft patch set is viewed and the user is the change owner or has the
-link:access-control.html#category_delete_drafts[Delete Drafts] access
-right assigned.
+For normal changes, the `Delete Change` button will only be available if the
+user is an administrator and the change hasn't been merged. For draft changes,
+the `Delete Change` / `Delete Revision` buttons will be available if the user
+is the change owner or has the
+link:access-control.html#category_delete_drafts[Delete Drafts] access right
+assigned.
 
 ** [[plugin-actions]]Further actions may be available if plugins are installed.
 
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index b7358d6..2cc64d8 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -696,6 +696,16 @@
         identifiedUserFactory.create(account.getId()));
   }
 
+  /**
+   * Enforce a new request context for the current API user.
+   *
+   * <p>This recreates the IdentifiedUser, hence everything which is cached in the IdentifiedUser is
+   * reloaded (e.g. the email addresses of the user).
+   */
+  protected Context resetCurrentApiUser() {
+    return atrScope.set(newRequestContext(atrScope.get().getSession().getAccount()));
+  }
+
   protected Context setApiUser(TestAccount account) {
     return atrScope.set(newRequestContext(account));
   }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
index 1f00248..f7369d7 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -111,4 +111,8 @@
     b.append(session.getPort());
     return b.toString();
   }
+
+  public TestAccount getAccount() {
+    return account;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
index 705ad4f..12fa3cc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
@@ -36,8 +36,11 @@
   }
 
   @Test
-  @GerritConfig(name = "x.y", values = { "a", "b" })
+  @GerritConfig(
+    name = "x.y",
+    values = {"a", "b"}
+  )
   public void testList() {
-    assertThat(cfg.getStringList("x", null, "y")).asList().containsExactly("a" , "b");
+    assertThat(cfg.getStringList("x", null, "y")).asList().containsExactly("a", "b");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 11b452e..82aa576 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -386,9 +386,7 @@
       gApi.accounts().self().addEmail(input);
     }
 
-    // enforce a new request context so that emails that are cached in
-    // IdentifiedUser are reloaded
-    setApiUser(admin);
+    resetCurrentApiUser();
     assertThat(getEmails()).containsAllIn(emails);
   }
 
@@ -428,20 +426,67 @@
     input.noConfirmation = true;
     gApi.accounts().self().addEmail(input);
 
-    // enforce a new request context so that emails that are cached in
-    // IdentifiedUser are reloaded
-    setApiUser(admin);
+    resetCurrentApiUser();
     assertThat(getEmails()).contains(email);
 
     gApi.accounts().self().deleteEmail(input.email);
 
-    // enforce a new request context so that emails that are cached in
-    // IdentifiedUser are reloaded
-    setApiUser(admin);
+    resetCurrentApiUser();
     assertThat(getEmails()).doesNotContain(email);
   }
 
   @Test
+  public void deleteEmailFromCustomExternalIdSchemes() throws Exception {
+    String email = "foo.bar@example.com";
+    String extId1 = "foo:bar";
+    String extId2 = "foo:baz";
+    db.accountExternalIds()
+        .insert(
+            ImmutableList.of(
+                createExternalIdWithEmail(extId1, email),
+                createExternalIdWithEmail(extId2, email)));
+    accountCache.evict(admin.id);
+    assertThat(
+            gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
+        .containsAllOf(extId1, extId2);
+
+    resetCurrentApiUser();
+    assertThat(getEmails()).contains(email);
+
+    gApi.accounts().self().deleteEmail(email);
+
+    resetCurrentApiUser();
+    assertThat(getEmails()).doesNotContain(email);
+    assertThat(
+            gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
+        .containsNoneOf(extId1, extId2);
+  }
+
+  @Test
+  public void deleteEmailOfOtherUser() throws Exception {
+    String email = "foo.bar@example.com";
+    EmailInput input = new EmailInput();
+    input.email = email;
+    input.noConfirmation = true;
+    gApi.accounts().id(user.id.get()).addEmail(input);
+
+    setApiUser(user);
+    assertThat(getEmails()).contains(email);
+
+    // admin can delete email of user
+    setApiUser(admin);
+    gApi.accounts().id(user.id.get()).deleteEmail(email);
+
+    setApiUser(user);
+    assertThat(getEmails()).doesNotContain(email);
+
+    // user cannot delete email of admin
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to delete email address");
+    gApi.accounts().id(admin.id.get()).deleteEmail(admin.email);
+  }
+
+  @Test
   public void putStatus() throws Exception {
     List<String> statuses = ImmutableList.of("OOO", "Busy");
     AccountInfo info;
@@ -886,4 +931,10 @@
   private Set<String> getEmails() throws RestApiException {
     return gApi.accounts().self().getEmails().stream().map(e -> e.email).collect(toSet());
   }
+
+  private AccountExternalId createExternalIdWithEmail(String id, String email) {
+    AccountExternalId extId = new AccountExternalId(admin.id, new AccountExternalId.Key(id));
+    extId.setEmailAddress(email);
+    return extId;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
index fe80032..fcf939d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.client.Theme;
@@ -35,6 +36,7 @@
 import org.junit.Test;
 
 @NoHttpd
+@Sandboxed
 public class DiffPreferencesIT extends AbstractDaemonTest {
   @Inject private AllUsersName allUsers;
 
@@ -136,4 +138,39 @@
     // assert hard-coded defaults
     assertPrefs(o, d, "lineLength", "tabSize", "fontSize");
   }
+
+  @Test
+  public void overwriteConfiguredDefaults() throws Exception {
+    DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
+    int configuredDefaultLineLength = d.lineLength + 10;
+    DiffPreferencesInfo update = new DiffPreferencesInfo();
+    update.lineLength = configuredDefaultLineLength;
+    gApi.config().server().setDefaultDiffPreferences(update);
+
+    DiffPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    assertThat(o.lineLength).isEqualTo(configuredDefaultLineLength);
+    assertPrefs(o, d, "lineLength");
+
+    int newLineLength = configuredDefaultLineLength + 10;
+    DiffPreferencesInfo i = new DiffPreferencesInfo();
+    i.lineLength = newLineLength;
+    DiffPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    assertThat(a.lineLength).isEqualTo(newLineLength);
+    assertPrefs(a, d, "lineLength");
+
+    a = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    assertThat(a.lineLength).isEqualTo(newLineLength);
+    assertPrefs(a, d, "lineLength");
+
+    // overwrite the configured default with original hard-coded default
+    i = new DiffPreferencesInfo();
+    i.lineLength = d.lineLength;
+    a = gApi.accounts().id(admin.getId().toString()).setDiffPreferences(i);
+    assertThat(a.lineLength).isEqualTo(d.lineLength);
+    assertPrefs(a, d, "lineLength");
+
+    a = gApi.accounts().id(admin.getId().toString()).getDiffPreferences();
+    assertThat(a.lineLength).isEqualTo(d.lineLength);
+    assertPrefs(a, d, "lineLength");
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 334f336..8ed2a45 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -19,6 +19,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
@@ -41,6 +42,7 @@
 import org.junit.Test;
 
 @NoHttpd
+@Sandboxed
 public class GeneralPreferencesIT extends AbstractDaemonTest {
   @Inject private AllUsersName allUsers;
 
@@ -122,4 +124,39 @@
     // assert hard-coded defaults
     assertPrefs(o, d, "my", "changeTable", "changesPerPage");
   }
+
+  @Test
+  public void overwriteConfiguredDefaults() throws Exception {
+    GeneralPreferencesInfo d = GeneralPreferencesInfo.defaults();
+    int configuredChangesPerPage = d.changesPerPage * 2;
+    GeneralPreferencesInfo update = new GeneralPreferencesInfo();
+    update.changesPerPage = configuredChangesPerPage;
+    gApi.config().server().setDefaultPreferences(update);
+
+    GeneralPreferencesInfo o = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    assertThat(o.changesPerPage).isEqualTo(configuredChangesPerPage);
+    assertPrefs(o, d, "my", "changeTable", "changesPerPage");
+
+    int newChangesPerPage = configuredChangesPerPage * 2;
+    GeneralPreferencesInfo i = new GeneralPreferencesInfo();
+    i.changesPerPage = newChangesPerPage;
+    GeneralPreferencesInfo a = gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
+    assertPrefs(a, d, "my", "changeTable", "changesPerPage");
+
+    a = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
+    assertPrefs(a, d, "my", "changeTable", "changesPerPage");
+
+    // overwrite the configured default with original hard-coded default
+    i = new GeneralPreferencesInfo();
+    i.changesPerPage = d.changesPerPage;
+    a = gApi.accounts().id(admin.getId().toString()).setPreferences(i);
+    assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
+    assertPrefs(a, d, "my", "changeTable", "changesPerPage");
+
+    a = gApi.accounts().id(admin.getId().toString()).getPreferences();
+    assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
+    assertPrefs(a, d, "my", "changeTable", "changesPerPage");
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EmailIT.java
index f9d3357..186d625 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -42,7 +42,7 @@
     String email = "foo.bar2@example.com";
     assertThat(getEmails()).doesNotContain(email);
 
-    createEmail(email.replaceAll("@", "%40"));
+    createEmail(email.replace("@", "%40"));
     assertThat(getEmails()).contains(email);
   }
 
@@ -67,8 +67,7 @@
     createEmail(email);
     assertThat(getEmails()).contains(email);
 
-    RestResponse r =
-        adminRestSession.delete("/accounts/self/emails/" + email.replaceAll("@", "%40"));
+    RestResponse r = adminRestSession.delete("/accounts/self/emails/" + email.replace("@", "%40"));
     r.assertNoContent();
     assertThat(getEmails()).doesNotContain(email);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index ece30e9..f05ecce 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -15,12 +15,15 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 import org.junit.Test;
 
 public class KillTaskIT extends AbstractDaemonTest {
@@ -30,17 +33,24 @@
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(), new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
-    int taskCount = result.size();
-    assertThat(taskCount).isGreaterThan(0);
 
-    r = adminRestSession.delete("/config/server/tasks/" + result.get(0).id);
+    Optional<String> id =
+        result
+            .stream()
+            .filter(t -> "Log File Compressor".equals(t.command))
+            .map(t -> t.id)
+            .findFirst();
+    assertThat(id.isPresent()).isTrue();
+
+    r = adminRestSession.delete("/config/server/tasks/" + id.get());
     r.assertNoContent();
     r.consume();
 
     r = adminRestSession.get("/config/server/tasks/");
     result = newGson().fromJson(r.getReader(), new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
-    assertThat(result).hasSize(taskCount - 1);
+    Set<String> ids = result.stream().map(t -> t.id).collect(toSet());
+    assertThat(ids).doesNotContain(id.get());
   }
 
   private void killTask_NotFound() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 7db5b9e..d423a2d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -208,6 +208,35 @@
     gApi.accounts().id("user").setActive(true);
   }
 
+  @Test
+  public void sendNotificationAfterPersistingComments() throws Exception {
+    String changeId = createChangeWithReview();
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
+    assertThat(comments).hasSize(2);
+    String ts =
+        MailUtil.rfcDateformatter.format(
+            ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
+
+    // Build Message
+    String txt =
+        newPlaintextBody(
+            canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
+            "Test Message",
+            null,
+            null,
+            null);
+    MailMessage.Builder b =
+        messageBuilderWithDefaultFields()
+            .from(user.emailAddress)
+            .textContent(txt + textFooterForChange(changeId, ts));
+
+    sender.clear();
+    mailProcessor.process(b.build());
+
+    assertNotifyTo(admin);
+  }
+
   private static CommentInput newComment(String path, Side side, int line, String message) {
     CommentInput c = new CommentInput();
     c.path = path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index 96c4b8d..8541cf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import static java.util.stream.Collectors.toSet;
+
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -31,6 +33,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Set;
 
 @Singleton
 public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
@@ -69,13 +72,26 @@
     if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow deleting emails");
     }
-    AccountExternalId.Key key = new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
-    AccountExternalId extId = dbProvider.get().accountExternalIds().get(key);
-    if (extId == null) {
+
+    Set<AccountExternalId> extIds =
+        dbProvider
+            .get()
+            .accountExternalIds()
+            .byAccount(user.getAccountId())
+            .toList()
+            .stream()
+            .filter(e -> email.equals(e.getEmailAddress()))
+            .collect(toSet());
+    if (extIds.isEmpty()) {
       throw new ResourceNotFoundException(email);
     }
+
     try {
-      accountManager.unlink(user.getAccountId(), AuthRequest.forEmail(email));
+      for (AccountExternalId extId : extIds) {
+        AuthRequest authRequest = new AuthRequest(extId.getKey().get());
+        authRequest.setEmailAddress(email);
+        accountManager.unlink(user.getAccountId(), authRequest);
+      }
     } catch (AccountException e) {
       throw new ResourceConflictException(e.getMessage());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
index 1c44670..df64c0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
@@ -77,14 +77,6 @@
       // Load all users default prefs
       VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
       dp.load(allUsers);
-      GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
-      loadSection(
-          dp.getConfig(),
-          UserConfigSections.GENERAL,
-          null,
-          allUserPrefs,
-          GeneralPreferencesInfo.defaults(),
-          in);
 
       // Load user prefs
       VersionedAccountPreferences p = VersionedAccountPreferences.forUser(id);
@@ -95,13 +87,33 @@
               UserConfigSections.GENERAL,
               null,
               new GeneralPreferencesInfo(),
-              updateDefaults(allUserPrefs),
+              readDefaultsFromGit(dp.getConfig(), in),
               in);
       loadChangeTableColumns(r, p, dp);
       return loadMyMenusAndUrlAliases(r, p, dp);
     }
   }
 
+  public GeneralPreferencesInfo readDefaultsFromGit(Repository git, GeneralPreferencesInfo in)
+      throws ConfigInvalidException, IOException {
+    VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
+    dp.load(git);
+    return readDefaultsFromGit(dp.getConfig(), in);
+  }
+
+  private GeneralPreferencesInfo readDefaultsFromGit(Config config, GeneralPreferencesInfo in)
+      throws ConfigInvalidException {
+    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
+    loadSection(
+        config,
+        UserConfigSections.GENERAL,
+        null,
+        allUserPrefs,
+        GeneralPreferencesInfo.defaults(),
+        in);
+    return updateDefaults(allUserPrefs);
+  }
+
   private GeneralPreferencesInfo updateDefaults(GeneralPreferencesInfo input) {
     GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
index 5a39bea..0edff4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -69,28 +69,30 @@
       Account.Id id, GitRepositoryManager gitMgr, AllUsersName allUsersName, DiffPreferencesInfo in)
       throws IOException, ConfigInvalidException, RepositoryNotFoundException {
     try (Repository git = gitMgr.openRepository(allUsersName)) {
-      // Load all users prefs.
-      VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
-      dp.load(git);
-      DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
-      loadSection(
-          dp.getConfig(),
-          UserConfigSections.DIFF,
-          null,
-          allUserPrefs,
-          DiffPreferencesInfo.defaults(),
-          in);
-
-      // Load user prefs
       VersionedAccountPreferences p = VersionedAccountPreferences.forUser(id);
       p.load(git);
       DiffPreferencesInfo prefs = new DiffPreferencesInfo();
       loadSection(
-          p.getConfig(), UserConfigSections.DIFF, null, prefs, updateDefaults(allUserPrefs), in);
+          p.getConfig(), UserConfigSections.DIFF, null, prefs, readDefaultsFromGit(git, in), in);
       return prefs;
     }
   }
 
+  static DiffPreferencesInfo readDefaultsFromGit(Repository git, DiffPreferencesInfo in)
+      throws ConfigInvalidException, IOException {
+    VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
+    dp.load(git);
+    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
+    loadSection(
+        dp.getConfig(),
+        UserConfigSections.DIFF,
+        null,
+        allUserPrefs,
+        DiffPreferencesInfo.defaults(),
+        in);
+    return updateDefaults(allUserPrefs);
+  }
+
   private static DiffPreferencesInfo updateDefaults(DiffPreferencesInfo input) {
     DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index 986124a..ac0cc96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.gerrit.server.account.GetDiffPreferences.readDefaultsFromGit;
 import static com.google.gerrit.server.account.GetDiffPreferences.readFromGit;
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
 import static com.google.gerrit.server.config.ConfigUtil.storeSection;
@@ -74,18 +75,12 @@
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
     DiffPreferencesInfo out = new DiffPreferencesInfo();
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
+      DiffPreferencesInfo allUserPrefs = readDefaultsFromGit(md.getRepository(), null);
       VersionedAccountPreferences prefs = VersionedAccountPreferences.forUser(userId);
       prefs.load(md);
-      DiffPreferencesInfo defaults = DiffPreferencesInfo.defaults();
-      storeSection(prefs.getConfig(), UserConfigSections.DIFF, null, in, defaults);
+      storeSection(prefs.getConfig(), UserConfigSections.DIFF, null, in, allUserPrefs);
       prefs.commit(md);
-      loadSection(
-          prefs.getConfig(),
-          UserConfigSections.DIFF,
-          null,
-          out,
-          DiffPreferencesInfo.defaults(),
-          null);
+      loadSection(prefs.getConfig(), UserConfigSections.DIFF, null, out, allUserPrefs, null);
     }
     return out;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index e1ed6aa..91672f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -104,7 +104,7 @@
           UserConfigSections.GENERAL,
           null,
           i,
-          GeneralPreferencesInfo.defaults());
+          loader.readDefaultsFromGit(md.getRepository(), null));
 
       storeMyChangeTableColumns(prefs, i.changeTable);
       storeMyMenus(prefs, i.my);
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 8e1ab10..be719c5 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
@@ -479,7 +479,7 @@
     out._number = in.getId().get();
 
     if (user.isIdentifiedUser()) {
-      Collection<String> stars = cd.stars().get(user.getAccountId());
+      Collection<String> stars = cd.stars(user.getAccountId());
       out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
       if (!stars.isEmpty()) {
         out.stars = stars;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index 392db81..2e8fc2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -49,7 +49,7 @@
 public class EmailReviewComments implements Runnable, RequestContext {
   private static final Logger log = LoggerFactory.getLogger(EmailReviewComments.class);
 
-  interface Factory {
+  public interface Factory {
     EmailReviewComments create(
         NotifyHandling notify,
         ListMultimap<RecipientType, Account.Id> accountsToNotify,
@@ -111,7 +111,7 @@
     this.labels = labels;
   }
 
-  void sendAsync() {
+  public void sendAsync() {
     @SuppressWarnings("unused")
     Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(this);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
index 14e1f92..843ef3c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -140,8 +140,8 @@
    * Partition the reference tips into two sets:
    *
    * <ul>
-   *   <li> before = commits with time < target.getCommitTime()
-   *   <li> after = commits with time >= target.getCommitTime()
+   *   <li>before = commits with time < target.getCommitTime()
+   *   <li>after = commits with time >= target.getCommitTime()
    * </ul>
    *
    * Each of the before/after lists is sorted by the the commit time.
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 c7e0222..2526db194 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
@@ -304,9 +304,9 @@
    * <p>This adds the following footers if they are missing:
    *
    * <ul>
-   *   <li> Reviewed-on: <i>url</i>
-   *   <li> Reviewed-by | Tested-by | <i>Other-Label-Name</i>: <i>reviewer</i>
-   *   <li> Change-Id
+   *   <li>Reviewed-on: <i>url</i>
+   *   <li>Reviewed-by | Tested-by | <i>Other-Label-Name</i>: <i>reviewer</i>
+   *   <li>Change-Id
    * </ul>
    *
    * @param n
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index f4c0867..2316782 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -109,6 +109,7 @@
       ImmutableSet.of(
           // Required for ChangeIsVisibleToPrdicate.
           ChangeField.CHANGE.getName(),
+          ChangeField.REVIEWER.getName(),
           // Required during advertiseOpenChanges.
           ChangeField.PATCH_SET.getName());
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
index 20e163e..163d414 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
@@ -90,12 +90,13 @@
           && elementName.equals("div")
           && !e.className().startsWith("gmail")) {
         // This is a comment typed by the user
-        String content = e.ownText().trim();
+        // Replace non-breaking spaces and trim string
+        String content = e.ownText() .replace('\u00a0', ' ').trim();
         if (!Strings.isNullOrEmpty(content)) {
           if (lastEncounteredComment == null && lastEncounteredFileName == null) {
             // Remove quotation line, email signature and
             // "Sent from my xyz device"
-            content = ParserUtil.trimQuotationLine(content);
+            content = ParserUtil.trimQuotation(content);
             // TODO(hiesel) Add more sanitizer
             if (!Strings.isNullOrEmpty(content)) {
               parsedComments.add(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 08bd0ba..54b30c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -15,7 +15,10 @@
 package com.google.gerrit.server.mail.receive;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -28,16 +31,19 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.git.BatchUpdate;
-import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.mail.MailFilter;
 import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -48,8 +54,10 @@
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.slf4j.Logger;
@@ -69,6 +77,9 @@
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<ReviewDb> reviewDb;
   private final DynamicMap<MailFilter> mailFilters;
+  private final EmailReviewComments.Factory outgoingMailFactory;
+  private final CommentAdded commentAdded;
+  private final ApprovalsUtil approvalsUtil;
   private final Provider<String> canonicalUrl;
 
   @Inject
@@ -83,6 +94,9 @@
       Provider<InternalChangeQuery> queryProvider,
       Provider<ReviewDb> reviewDb,
       DynamicMap<MailFilter> mailFilters,
+      EmailReviewComments.Factory outgoingMailFactory,
+      ApprovalsUtil approvalsUtil,
+      CommentAdded commentAdded,
       @CanonicalWebUrl Provider<String> canonicalUrl) {
     this.accountByEmailCache = accountByEmailCache;
     this.buf = buf;
@@ -94,6 +108,9 @@
     this.queryProvider = queryProvider;
     this.reviewDb = reviewDb;
     this.mailFilters = mailFilters;
+    this.outgoingMailFactory = outgoingMailFactory;
+    this.commentAdded = commentAdded;
+    this.approvalsUtil = approvalsUtil;
     this.canonicalUrl = canonicalUrl;
   }
 
@@ -192,6 +209,10 @@
     private final PatchSet.Id psId;
     private final List<MailComment> parsedComments;
     private final String tag;
+    private ChangeMessage changeMessage;
+    private List<Comment> comments;
+    private PatchSet patchSet;
+    private ChangeControl changeControl;
 
     private Op(PatchSet.Id psId, List<MailComment> parsedComments, String messageId) {
       this.psId = psId;
@@ -200,10 +221,11 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
+    public boolean updateChange(BatchUpdate.ChangeContext ctx)
         throws OrmException, UnprocessableEntityException {
-      PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
-      if (ps == null) {
+      changeControl = ctx.getControl();
+      patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
+      if (patchSet == null) {
         throw new OrmException("patch set not found: " + psId);
       }
 
@@ -217,10 +239,9 @@
         changeMsg += "\n" + numComments(parsedComments.size());
       }
 
-      ChangeMessage msg = ChangeMessagesUtil.newMessage(ctx, changeMsg, tag);
-      changeMessagesUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), msg);
-
-      List<Comment> comments = new ArrayList<>();
+      changeMessage = ChangeMessagesUtil.newMessage(ctx, changeMsg, tag);
+      changeMessagesUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
+      comments = new ArrayList<>();
       for (MailComment c : parsedComments) {
         if (c.type == MailComment.CommentType.CHANGE_MESSAGE) {
           continue;
@@ -241,7 +262,7 @@
           side = Side.fromShort(c.inReplyTo.side);
         } else {
           fileName = c.fileName;
-          psForComment = ps;
+          psForComment = patchSet;
           side = Side.REVISION;
         }
 
@@ -272,6 +293,38 @@
 
       return true;
     }
+
+    @Override
+    public void postUpdate(BatchUpdate.Context ctx) throws Exception {
+      // Send email notifications
+      outgoingMailFactory
+          .create(
+              NotifyHandling.ALL,
+              ArrayListMultimap.create(),
+              changeControl.getNotes(),
+              patchSet,
+              ctx.getUser().asIdentifiedUser(),
+              changeMessage,
+              comments,
+              null,
+              ImmutableList.of())
+          .sendAsync();
+      // Get previous approvals from this user
+      Map<String, Short> approvals = new HashMap<>();
+      approvalsUtil
+          .byPatchSetUser(ctx.getDb(), changeControl, psId, ctx.getAccountId())
+          .forEach(a -> approvals.put(a.getLabel(), a.getValue()));
+      // Fire Gerrit event. Note that approvals can't be granted via email, so old and new approvals
+      // are always the same here.
+      commentAdded.fire(
+          changeControl.getChange(),
+          patchSet,
+          ctx.getAccount(),
+          changeMessage.getMessage(),
+          approvals,
+          approvals,
+          ctx.getWhen());
+    }
   }
 
   private static boolean useHtmlParser(MailMessage m) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ParserUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ParserUtil.java
index 72eb18a..bfead94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ParserUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ParserUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.receive;
 
 import com.google.gerrit.reviewdb.client.Comment;
+import java.util.StringJoiner;
 import java.util.regex.Pattern;
 
 public class ParserUtil {
@@ -24,39 +25,44 @@
               + "(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})");
 
   /**
-   * Trims the quotation line that email clients add Example: On Sun, Nov 20, 2016 at 10:33 PM,
-   * <gerrit@hiesel.it> wrote:
+   * Trims the quotation that email clients add Example: On Sun, Nov 20, 2016 at 10:33 PM,
+   * <gerrit@gerritcodereview.com> wrote:
    *
    * @param comment Comment parsed from an email.
    * @return Trimmed comment.
    */
-  public static String trimQuotationLine(String comment) {
-    // Identifying the quotation line is hard, as it can be in any language.
-    // We identify this line by it's characteristics: It usually contains a
-    // valid email address, some digits for the date in groups of 1-4 in a row
-    // as well as some characters.
-    StringBuilder b = new StringBuilder();
-    for (String line : comment.split("\n")) {
-      // Count occurrences of digit groups
-      int numConsecutiveDigits = 0;
-      int maxConsecutiveDigits = 0;
-      int numDigitGroups = 0;
-      for (char c : line.toCharArray()) {
-        if (c >= '0' && c <= '9') {
-          numConsecutiveDigits++;
-        } else if (numConsecutiveDigits > 0) {
-          maxConsecutiveDigits = Integer.max(maxConsecutiveDigits, numConsecutiveDigits);
-          numConsecutiveDigits = 0;
-          numDigitGroups++;
-        }
+  public static String trimQuotation(String comment) {
+    StringJoiner j = new StringJoiner("\n");
+    String[] lines = comment.split("\n");
+    for (int i = 0; i < lines.length - 2; i++) {
+      j.add(lines[i]);
+    }
+
+    // Check if the last line contains the full quotation pattern (date + email)
+    String lastLine = lines[lines.length - 1];
+    if (containsQuotationPattern(lastLine)) {
+      if (lines.length > 1) {
+        j.add(lines[lines.length - 2]);
       }
-      if (numDigitGroups < 4
-          || maxConsecutiveDigits > 4
-          || !SIMPLE_EMAIL_PATTERN.matcher(line).find()) {
-        b.append(line);
+      return j.toString().trim();
+    }
+
+    // Check if the second last line + the last line contain the full quotation pattern. This is
+    // necessary, as the quotation line can be split across the last two lines if it gets too long.
+    if (lines.length > 1) {
+      String lastLines = lines[lines.length - 2] + lastLine;
+      if (containsQuotationPattern(lastLines)) {
+        return j.toString().trim();
       }
     }
-    return b.toString().trim();
+
+    // Add the last two lines
+    if (lines.length > 1) {
+      j.add(lines[lines.length - 2]);
+    }
+    j.add(lines[lines.length - 1]);
+
+    return j.toString().trim();
   }
 
   /** Check if string is an inline comment url on a patch set or the base */
@@ -69,4 +75,31 @@
   public static String filePath(String changeUrl, Comment comment) {
     return changeUrl + "/" + comment.key.patchSetId + "/" + comment.key.filename;
   }
+
+  private static boolean containsQuotationPattern(String s) {
+    // Identifying the quotation line is hard, as it can be in any language.
+    // We identify this line by it's characteristics: It usually contains a
+    // valid email address, some digits for the date in groups of 1-4 in a row
+    // as well as some characters.
+
+    // Count occurrences of digit groups
+    int numConsecutiveDigits = 0;
+    int maxConsecutiveDigits = 0;
+    int numDigitGroups = 0;
+    for (char c : s.toCharArray()) {
+      if (c >= '0' && c <= '9') {
+        numConsecutiveDigits++;
+      } else if (numConsecutiveDigits > 0) {
+        maxConsecutiveDigits = Integer.max(maxConsecutiveDigits, numConsecutiveDigits);
+        numConsecutiveDigits = 0;
+        numDigitGroups++;
+      }
+    }
+    if (numDigitGroups < 4 || maxConsecutiveDigits > 4) {
+      return false;
+    }
+
+    // Check if the string contains an email address
+    return SIMPLE_EMAIL_PATTERN.matcher(s).find();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/TextParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/TextParser.java
index a752e89..fa33cc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/TextParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/TextParser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.receive;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.PeekingIterator;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -64,12 +65,21 @@
     String lastEncounteredFileName = null;
     Comment lastEncounteredComment = null;
     for (String line : lines) {
+      if (line.equals(">")) {
+        // Skip empty lines
+        continue;
+      }
       if (line.startsWith("> ")) {
         line = line.substring("> ".length()).trim();
         // This is not a comment, try to advance the file/comment pointers and
         // add previous comment to list if applicable
         if (currentComment != null) {
-          parsedComments.add(currentComment);
+          if (currentComment.type == MailComment.CommentType.CHANGE_MESSAGE) {
+            currentComment.message = ParserUtil.trimQuotation(currentComment.message);
+          }
+          if (!Strings.isNullOrEmpty(currentComment.message)) {
+            parsedComments.add(currentComment);
+          }
           currentComment = null;
         }
 
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 2263873..17e43f2 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
@@ -24,6 +24,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
@@ -342,8 +344,8 @@
   private Map<Account.Id, Ref> editsByUser;
   private Set<Account.Id> reviewedBy;
   private Map<Account.Id, Ref> draftsByUser;
-  @Deprecated private Set<Account.Id> starredByUser;
   private ImmutableListMultimap<Account.Id, String> stars;
+  private StarsOf starsOf;
   private ImmutableMap<Account.Id, StarRef> starRefs;
   private ReviewerSet reviewers;
   private List<ReviewerStatusUpdate> reviewerUpdates;
@@ -1180,23 +1182,6 @@
     this.hashtags = hashtags;
   }
 
-  @Deprecated
-  public Set<Account.Id> starredBy() throws OrmException {
-    if (starredByUser == null) {
-      if (!lazyLoad) {
-        return Collections.emptySet();
-      }
-      starredByUser =
-          checkNotNull(starredChangesUtil).byChange(legacyId, StarredChangesUtil.DEFAULT_LABEL);
-    }
-    return starredByUser;
-  }
-
-  @Deprecated
-  public void setStarredBy(Set<Account.Id> starredByUser) {
-    this.starredByUser = starredByUser;
-  }
-
   public ImmutableListMultimap<Account.Id, String> stars() throws OrmException {
     if (stars == null) {
       if (!lazyLoad) {
@@ -1225,15 +1210,23 @@
     return starRefs;
   }
 
-  @AutoValue
-  abstract static class ReviewedByEvent {
-    private static ReviewedByEvent create(ChangeMessage msg) {
-      return new AutoValue_ChangeData_ReviewedByEvent(msg.getAuthor(), msg.getWrittenOn());
+  public Set<String> stars(Account.Id accountId) throws OrmException {
+    if (starsOf != null) {
+      if (!starsOf.accountId().equals(accountId)) {
+        starsOf = null;
+      }
     }
-
-    public abstract Account.Id author();
-
-    public abstract Timestamp ts();
+    if (starsOf == null) {
+      if (stars != null) {
+        starsOf = StarsOf.create(accountId, stars.get(accountId));
+      } else {
+        if (!lazyLoad) {
+          return ImmutableSet.of();
+        }
+        starsOf = StarsOf.create(accountId, starredChangesUtil.getLabels(accountId, legacyId));
+      }
+    }
+    return starsOf.stars();
   }
 
   @Override
@@ -1272,4 +1265,26 @@
   public void setRefStatePatterns(Iterable<byte[]> refStatePatterns) {
     this.refStatePatterns = ImmutableList.copyOf(refStatePatterns);
   }
+
+  @AutoValue
+  abstract static class ReviewedByEvent {
+    private static ReviewedByEvent create(ChangeMessage msg) {
+      return new AutoValue_ChangeData_ReviewedByEvent(msg.getAuthor(), msg.getWrittenOn());
+    }
+
+    public abstract Account.Id author();
+
+    public abstract Timestamp ts();
+  }
+
+  @AutoValue
+  abstract static class StarsOf {
+    private static StarsOf create(Account.Id accountId, Iterable<String> stars) {
+      return new AutoValue_ChangeData_StarsOf(accountId, ImmutableSortedSet.copyOf(stars));
+    }
+
+    public abstract Account.Id accountId();
+
+    public abstract ImmutableSortedSet<String> stars();
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java
index 7d729bc..11bc4ff 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java
@@ -41,7 +41,7 @@
     b.htmlContent(
         newHtmlBody(
             "Looks good to me",
-            "I have a comment on this.",
+            "I have a comment on this.&nbsp;",
             null,
             "Also have a comment here.",
             null,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/ParserUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/ParserUtilTest.java
new file mode 100644
index 0000000..dfa492c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/ParserUtilTest.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail.receive;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class ParserUtilTest {
+  @Test
+  public void trimQuotationLineOnMessageWithoutQuoatationLine() throws Exception {
+    assertThat(ParserUtil.trimQuotation("One line")).isEqualTo("One line");
+    assertThat(ParserUtil.trimQuotation("Two\nlines")).isEqualTo("Two\nlines");
+    assertThat(ParserUtil.trimQuotation("Thr\nee\nlines")).isEqualTo("Thr\nee\nlines");
+  }
+
+  @Test
+  public void trimQuotationLineOnMixedMessages() throws Exception {
+    assertThat(
+            ParserUtil.trimQuotation(
+                "One line\n"
+                    + "On Thu, Feb 9, 2017 at 8:21 AM, ekempin (Gerrit)\n"
+                    + "<noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com> wrote:"))
+        .isEqualTo("One line");
+    assertThat(
+            ParserUtil.trimQuotation(
+                "One line\n"
+                    + "On Thu, Feb 9, 2017 at 8:21 AM, ekempin (Gerrit) "
+                    + "<noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com> wrote:"))
+        .isEqualTo("One line");
+  }
+
+  @Test
+  public void trimQuotationLineOnMessagesContainingQuoationLine() throws Exception {
+    assertThat(
+            ParserUtil.trimQuotation(
+                "On Thu, Feb 9, 2017 at 8:21 AM, ekempin (Gerrit)\n"
+                    + "<noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com> wrote:"))
+        .isEqualTo("");
+    assertThat(
+            ParserUtil.trimQuotation(
+                "On Thu, Feb 9, 2017 at 8:21 AM, ekempin (Gerrit) "
+                    + "<noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com> wrote:"))
+        .isEqualTo("");
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/TextParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/TextParserTest.java
index b23b341..a98835b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/TextParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/TextParserTest.java
@@ -180,6 +180,8 @@
   private static String newPlaintextBody(
       String changeMessage, String c1, String c2, String c3, String f1, String f2, String fc1) {
     return (changeMessage == null ? "" : changeMessage + "\n")
+        + "On Thu, Feb 9, 2017 at 8:21 AM, ekempin (Gerrit)\n"
+        + "<noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com> wrote: \n"
         + "> Foo Bar has posted comments on this change. (  \n"
         + "> "
         + changeURL
@@ -217,6 +219,7 @@
         + "> Should entry.getKey() be included in this message?\n"
         + "> \n"
         + (c1 == null ? "" : c1 + "\n")
+        + ">\n"
         + "> \n"
         + "> "
         + changeURL
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 9f31550..ab3a98a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -84,6 +84,7 @@
           on-tap-item-cherrypick="_handleCherrypickTap"
           on-tap-item-delete="_handleDeleteTap"
           hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
+          disabled-ids="[[_disabledMenuActions]]"
           items="[[_menuActions]]">More</gr-dropdown>
       <section hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
         <template
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 148b724..aa90a09 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -170,6 +170,10 @@
         type: Array,
         value: function() { return []; },
       },
+      _disabledMenuActions: {
+        type: Array,
+        value: function() { return []; },
+      },
     },
 
     ActionType: ActionType,
@@ -281,6 +285,7 @@
           this._keyCount(revisionActionsChangeRecord) === 0 &&
               additionalActions.length === 0;
       this._actionLoadingMessage = null;
+      this._disabledMenuActions = [];
     },
 
     _getValuesFor: function(obj) {
@@ -563,9 +568,16 @@
     _setLoadingOnButtonWithKey: function(key) {
       this._actionLoadingMessage = this._computeLoadingLabel(key);
 
-      // Return a NoOp for menu keys. @see Issue 5366
-      if (MENU_ACTION_KEYS.indexOf(key) !== -1) { return function() {}; }
+      // If the action appears in the overflow menu.
+      if (MENU_ACTION_KEYS.indexOf(key) !== -1) {
+        this.push('_disabledMenuActions', key === '/' ? 'delete' : key);
+        return function() {
+          this._actionLoadingMessage = null;
+          this._disabledMenuActions = [];
+        }.bind(this);
+      }
 
+      // Otherwise it's a top-level action.
       var buttonEl = this.$$('[data-action-key="' + key + '"]');
       buttonEl.setAttribute('loading', true);
       buttonEl.disabled = true;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index c1b582e..9d27ddc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -399,11 +399,16 @@
     });
 
     test('_setLoadingOnButtonWithKey overflow menu', function() {
-      // TODO(wyatta): Should not throw error when setting loading on an
-      // overflow action. @see Issue 5366
       var key = 'cherrypick';
       var cleanup = element._setLoadingOnButtonWithKey(key);
+      assert.equal(element._actionLoadingMessage, 'Cherry-Picking...');
+      assert.include(element._disabledMenuActions, 'cherrypick');
       assert.isFunction(cleanup);
+
+      cleanup();
+
+      assert.notOk(element._actionLoadingMessage);
+      assert.notInclude(element._disabledMenuActions, 'cherrypick');
     });
 
     suite('revert change', function() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 53a0c9e..6bd98ff 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -106,7 +106,7 @@
       }
       .commitMessage {
         font-family: var(--monospace-font-family);
-        flex: 1 0 72ch;
+        max-width: 100ch;
         margin-right: 2em;
         margin-bottom: 1em;
       }
@@ -188,6 +188,22 @@
       .patchInfo-header-wrapper {
         width: 100%;
       }
+      #commitMessage.collapsed {
+        max-height: 36em;
+        overflow: hidden;
+      }
+      .commitContainer {
+        display: flex;
+        flex-direction: column;
+      }
+      .collapseToggleContainer {
+        display: flex;
+        justify-content: center;
+      }
+      .collapseToggleContainer gr-button {
+        display: block;
+        width: 8em;
+      }
       @media screen and (max-width: 50em) {
         .mobile {
           display: block;
@@ -310,27 +326,44 @@
           </div>
           <hr class="mobile">
           <div class="commitAndRelated">
-            <div class="commitMessage">
-              <gr-editable-content id="commitMessageEditor"
-                  editing="[[_editingCommitMessage]]"
-                  content="{{_latestCommitMessage}}">
-                <gr-linked-text pre
-                    content="[[_latestCommitMessage]]"
-                    config="[[_projectConfig.commentlinks]]"
-                    remove-zero-width-space></gr-linked-text>
-              </gr-editable-content>
-              <gr-button link
-                  class="editCommitMessage"
-                  on-tap="_handleEditCommitMessage"
-                  hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
-              <div class="changeId" hidden$="[[!_changeIdCommitMessageError]]">
-                <hr>
-                Change-Id:
-                <span
-                    class$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]"
-                    title$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]">
-                  [[_change.change_id]]
-                </span>
+            <div class="commitContainer">
+              <div
+                  id="commitMessage"
+                  class$="commitMessage [[_computeCommitClass(_commitCollapsed, _latestCommitMessage)]]">
+                <gr-editable-content id="commitMessageEditor"
+                    editing="[[_editingCommitMessage]]"
+                    content="{{_latestCommitMessage}}"
+                    remove-zero-width-space>
+                  <gr-linked-text pre
+                      content="[[_latestCommitMessage]]"
+                      config="[[_projectConfig.commentlinks]]"
+                      remove-zero-width-space></gr-linked-text>
+                </gr-editable-content>
+                <gr-button link
+                    class="editCommitMessage"
+                    on-tap="_handleEditCommitMessage"
+                    hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
+                <div class="changeId" hidden$="[[!_changeIdCommitMessageError]]">
+                  <hr>
+                  Change-Id:
+                  <span
+                      class$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]"
+                      title$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]">
+                    [[_change.change_id]]
+                  </span>
+                </div>
+              </div>
+              <div
+                  id="commitCollapseToggle"
+                  class="collapseToggleContainer"
+                  hidden$="[[_computeCommitToggleHidden(_latestCommitMessage)]]">
+                <gr-button
+                    link
+                    id="commitCollapseToggleButton"
+                    class="collapseToggleButton"
+                    on-tap="_toggleCommitCollapsed">
+                  [[_computeCollapseCommitText(_commitCollapsed)]]
+                </gr-button>
               </div>
             </div>
             <div class="relatedChanges">
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 3ec5847..7c8d92e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -20,6 +20,9 @@
   };
   var CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm;
   var COMMENT_SAVE = 'Saving... Try again after all comments are saved.';
+
+  var MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
+
   // Maximum length for patch set descriptions.
   var PATCH_DESC_MAX_LENGTH = 500;
   var REVIEWERS_REGEX = /^R=/gm;
@@ -132,6 +135,10 @@
         type: String,
         computed: '_computeChangeStatus(_change, _patchRange.patchNum)',
       },
+      _commitCollapsed: {
+        type: Boolean,
+        value: true,
+      },
     },
 
     behaviors: [
@@ -686,15 +693,6 @@
       return label;
     },
 
-    _switchToMostRecentPatchNum: function() {
-      this._reload().then(function() {
-        var patchNum = this._computeLatestPatchNum(this._allPatchSets);
-        if (patchNum !== this._patchRange.patchNum) {
-          this._changePatchNum(patchNum);
-        }
-      }.bind(this));
-    },
-
     _handleAKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e) ||
@@ -714,9 +712,8 @@
 
     _handleCapitalRKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
       e.preventDefault();
-      this._switchToMostRecentPatchNum();
+      page.show('/c/' + this._change._number);
     },
 
     _handleSKey: function(e) {
@@ -839,6 +836,7 @@
     },
 
     _prepareCommitMsgForLinkify: function(msg) {
+      // TODO(wyatta) switch linkify sequence, see issue 5526.
       // This is a zero-with space. It is added to prevent the linkify library
       // from including R= as part of the email address.
       return msg.replace(REVIEWERS_REGEX, 'R=\u200B');
@@ -1025,5 +1023,23 @@
     _computeChangePermalinkAriaLabel: function(changeNum) {
       return 'Change ' + changeNum;
     },
+
+    _computeCommitClass: function(collapsed, commitMessage) {
+      if (this._computeCommitToggleHidden(commitMessage)) { return ''; }
+      return collapsed ? 'collapsed' : '';
+    },
+
+    _computeCollapseCommitText: function(collapsed) {
+      return collapsed ? 'Show more' : 'Show less';
+    },
+
+    _toggleCommitCollapsed: function() {
+      this._commitCollapsed = !this._commitCollapsed;
+    },
+
+    _computeCommitToggleHidden: function(commitMessage) {
+      if (!commitMessage) { return true; }
+      return commitMessage.split('\n').length < MIN_LINES_FOR_COMMIT_COLLAPSE;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 8162102..e36a55b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -101,16 +101,6 @@
 
       test('shift + R should fetch and navigate to the latest patch set',
           function(done) {
-        // Prevent all network requests to prevent random exceptions.
-        sandbox.stub(window, 'fetch', function() {
-          return Promise.resolve({
-            ok: true,
-            text: function() {
-              return Promise.resolve(')]}\'\n');
-            },
-          });
-        });
-
         element._changeNum = '42';
         element._patchRange = {
           basePatchNum: 'PARENT',
@@ -118,6 +108,7 @@
         };
         element._change = {
           change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+          _number: 42,
           revisions: {
             rev1: {_number: 1},
           },
@@ -128,23 +119,9 @@
         };
 
         sandbox.stub(element.$.actions, 'reload');
-        sandbox.stub(element.$.restAPI, '_getChangeDetail', function() {
-          // Mock change obj.
-          return Promise.resolve({
-            change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
-            revisions: {
-              rev1: {_number: 1, commit: {}},
-              rev13: {_number: 13},
-            },
-            current_revision: 'rev1',
-            status: 'NEW',
-            labels: {},
-            actions: {},
-          });
-        });
 
         var showStub = sandbox.stub(page, 'show', function(arg) {
-          assert.equal(arg, '/c/42/13');
+          assert.equal(arg, '/c/42');
           done();
         });
 
@@ -971,5 +948,28 @@
       element.serverConfig = {};
       assert.isFalse(element._replyDisabled);
     });
+
+    suite('commit message expand/collapse', function() {
+      test('commitCollapseToggle hidden for short commit message', function() {
+        element._latestCommitMessage = '';
+        assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden'));
+      });
+
+      test('commitCollapseToggle shown for long commit message', function() {
+        element._latestCommitMessage = _.times(31, String).join('\n');
+        assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden'));
+      });
+
+      test('commitCollapseToggle functions', function() {
+        element._latestCommitMessage = _.times(31, String).join('\n');
+        assert.isTrue(element._commitCollapsed);
+        assert.isTrue(
+            element.$.commitMessage.classList.contains('collapsed'));
+        MockInteractions.tap(element.$.commitCollapseToggleButton);
+        assert.isFalse(element._commitCollapsed);
+        assert.isFalse(
+            element.$.commitMessage.classList.contains('collapsed'));
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 94e15b6..8ec1055 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -198,6 +198,13 @@
             <td>Open download overlay</td>
           </tr>
           <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">r</span>
+            </td>
+            <td>Reload the change at the latest patch</td>
+          </tr>
+          <tr>
             <td></td><td class="header">File list</td>
           </tr>
           <tr>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
index 2c44813..adbb1a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
@@ -24,6 +24,9 @@
         display: block;
         white-space: normal;
       }
+      gr-diff-comment-thread + gr-diff-comment-thread {
+        margin-top: .2em;
+      }
     </style>
     <template is="dom-repeat" items="[[_threadGroups]]"
         as="thread">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
index bcd9b27..b9fc9f7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
@@ -74,6 +74,14 @@
     _sortByDate: function(threadGroups) {
       if (!threadGroups.length) { return; }
       return threadGroups.sort(function(a, b) {
+        // If a comment is a draft, it doesn't have a start_datetime yet.
+        // Assume it is newer than the comment it is being compared to.
+        if (!a.start_datetime) {
+          return 1;
+        }
+        if (!b.start_datetime) {
+          return -1;
+        }
         return util.parseDate(a.start_datetime) -
             util.parseDate(b.start_datetime);
       });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
index 7ae9b9d..79e58ee 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
@@ -165,6 +165,32 @@
       ];
 
       assert.deepEqual(element._sortByDate(threadGroups), expectedResult);
+
+      // When a comment doesn't have a date, the one without the date should be
+      // last.
+      var threadGroups = [
+        {
+          start_datetime: '2015-12-23 15:00:20.396000000',
+          comments: [],
+          locationRange: 'line',
+        },
+        {
+          comments: [],
+          locationRange: 'range-1-1-1-2',
+        },
+      ];
+
+      var expectedResult = [
+        {
+          start_datetime: '2015-12-23 15:00:20.396000000',
+          comments: [],
+          locationRange: 'line',
+        },
+        {
+          comments: [],
+          locationRange: 'range-1-1-1-2',
+        },
+      ];
     });
 
     test('_calculateLocationRange', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index fcd7574..7a60317 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -28,6 +28,10 @@
         margin-bottom: 1px;
         white-space: normal;
       }
+      .actions {
+        border-top: 1px dotted #bbb;
+        padding: .5em .7em;
+      }
       #container {
         background-color: #fcfad6;
       }
@@ -36,21 +40,31 @@
       }
     </style>
     <div id="container" class$="[[_computeHostClass(_unresolved)]]">
-      <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment">
+      <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
+          as="comment">
         <gr-diff-comment
             comment="{{comment}}"
+            robot-button-disabled="[[_hideActions(_showActions, _lastComment)]]"
             change-num="[[changeNum]]"
             patch-num="[[patchNum]]"
             draft="[[comment.__draft]]"
             show-actions="[[_showActions]]"
             comment-side="[[comment.__commentSide]]"
             project-config="[[projectConfig]]"
-            on-comment-discard="_handleCommentDiscard"
-            on-create-ack-comment="_handleCommentAck"
-            on-create-done-comment="_handleCommentDone"
             on-create-fix-comment="_handleCommentFix"
-            on-create-reply-comment="_handleCommentReply"></gr-diff-comment>
+            on-comment-discard="_handleCommentDiscard"></gr-diff-comment>
       </template>
+      <div class="actions"
+          hidden$="[[_hideActions(_showActions, _lastComment)]]">
+        <gr-button id="replyBtn" class="action reply"
+            on-tap="_handleCommentReply">Reply</gr-button>
+        <gr-button id="quoteBtn" class="action quote"
+            on-tap="_handleCommentQuote">Quote</gr-button>
+        <gr-button id="ackBtn" class="action ack" on-tap="_handleCommentAck">
+          Ack</gr-button>
+        <gr-button id="doneBtn" class="action done" on-tap="_handleCommentDone">
+          Done</gr-button>
+      </div>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index 7d91e91..fc130c7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -47,6 +47,7 @@
       },
 
       _showActions: Boolean,
+      _lastComment: Object,
       _orderedComments: Array,
       _unresolved: {
         type: Boolean,
@@ -77,22 +78,28 @@
       this._setInitialExpandedState();
     },
 
-    addOrEditDraft: function(opt_lineNum) {
-      var lastComment = this.comments[this.comments.length - 1];
-      if (lastComment && lastComment.__draft) {
+    addOrEditDraft: function(opt_lineNum, opt_range) {
+      var lastComment = this.comments[this.comments.length - 1] || {};
+      if (lastComment.__draft) {
         var commentEl = this._commentElWithDraftID(
             lastComment.id || lastComment.__draftID);
         commentEl.editing = true;
+
+        // If the comment was collapsed, re-open it to make it clear which
+        // actions are available.
+        commentEl.collapsed = false;
       } else {
-        this.addDraft(opt_lineNum,
-            lastComment ? lastComment.range : undefined);
+        var range = opt_range ? opt_range :
+            lastComment ? lastComment.range : undefined;
+        var unresolved = lastComment ? lastComment.unresolved : undefined;
+        this.addDraft(opt_lineNum, range, unresolved);
       }
     },
 
-    addDraft: function(opt_lineNum, opt_range) {
+    addDraft: function(opt_lineNum, opt_range, opt_unresolved) {
       var draft = this._newDraft(opt_lineNum, opt_range);
       draft.__editing = true;
-      draft.unresolved = true;
+      draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
       this.push('comments', draft);
     },
 
@@ -102,7 +109,14 @@
 
     _commentsChanged: function(changeRecord) {
       this._orderedComments = this._sortedComments(this.comments);
-      this._unresolved = this._getLastComment().unresolved;
+      if (this._orderedComments.length) {
+        this._lastComment = this._getLastComment();
+        this._unresolved = this._lastComment.unresolved;
+      }
+    },
+
+    _hideActions: function(_showActions, _lastComment) {
+      return !_showActions || !_lastComment || !!_lastComment.__draft;
     },
 
     _getLastComment: function() {
@@ -191,23 +205,31 @@
       }
     },
 
-    _handleCommentReply: function(e) {
-      var comment = e.detail.comment;
+    _processCommentReply: function(opt_quote) {
+      var comment = this._lastComment;
       var quoteStr;
-      if (e.detail.quote) {
+      if (opt_quote) {
         var msg = comment.message;
         quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
       }
       this._createReplyComment(comment, quoteStr, true, comment.unresolved);
     },
 
+    _handleCommentReply: function(e) {
+      this._processCommentReply();
+    },
+
+    _handleCommentQuote: function(e) {
+      this._processCommentReply(true);
+    },
+
     _handleCommentAck: function(e) {
-      var comment = e.detail.comment;
+      var comment = this._lastComment;
       this._createReplyComment(comment, 'Ack', false, comment.unresolved);
     },
 
     _handleCommentDone: function(e) {
-      var comment = e.detail.comment;
+      var comment = this._lastComment;
       this._createReplyComment(comment, 'Done', false, false);
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index ef4ab14..424cef6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -43,6 +43,7 @@
     var sandbox;
 
     setup(function() {
+      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getLoggedIn: function() { return Promise.resolve(false); },
       });
@@ -147,6 +148,17 @@
       assert.isFalse(commentElStub.called);
       assert.isTrue(addDraftStub.called);
     });
+
+    test('_hideActions', function() {
+      var showActions = true;
+      var lastComment = {};
+      assert.equal(element._hideActions(showActions, lastComment), false);
+      showActions = false;
+      assert.equal(element._hideActions(showActions, lastComment), true);
+      var showActions = true;
+      lastComment.__draft = true;
+      assert.equal(element._hideActions(showActions, lastComment), true);
+    });
   });
 
   suite('comment action tests', function() {
@@ -189,33 +201,35 @@
     test('reply', function(done) {
       var commentEl = element.$$('gr-diff-comment');
       assert.ok(commentEl);
-      commentEl.addEventListener('create-reply-comment', function() {
-        var drafts = element._orderedComments.filter(function(c) {
-          return c.__draft == true;
-        });
-        assert.equal(drafts.length, 1);
-        assert.notOk(drafts[0].message, 'message should be empty');
-        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-        done();
+
+      var replyBtn = element.$.replyBtn;
+      MockInteractions.tap(replyBtn);
+      flushAsynchronousOperations();
+
+      var drafts = element._orderedComments.filter(function(c) {
+        return c.__draft == true;
       });
-      commentEl.fire('create-reply-comment', {comment: commentEl.comment},
-          {bubbles: false});
+      assert.equal(drafts.length, 1);
+      assert.notOk(drafts[0].message, 'message should be empty');
+      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+      done();
     });
 
     test('quote reply', function(done) {
       var commentEl = element.$$('gr-diff-comment');
       assert.ok(commentEl);
-      commentEl.addEventListener('create-reply-comment', function() {
-        var drafts = element._orderedComments.filter(function(c) {
+
+      var quoteBtn = element.$.quoteBtn;
+      MockInteractions.tap(quoteBtn);
+      flushAsynchronousOperations();
+
+      var drafts = element._orderedComments.filter(function(c) {
           return c.__draft == true;
-        });
-        assert.equal(drafts.length, 1);
-        assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
-        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-        done();
       });
-      commentEl.fire('create-reply-comment', {comment: commentEl.comment,
-          quote: true}, {bubbles: false});
+      assert.equal(drafts.length, 1);
+      assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
+      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+      done();
     });
 
     test('quote reply multiline', function(done) {
@@ -233,27 +247,32 @@
 
       var commentEl = element.$$('gr-diff-comment');
       assert.ok(commentEl);
-      commentEl.addEventListener('create-reply-comment', function() {
-        var drafts = element._orderedComments.filter(function(c) {
-          return c.__draft == true;
-        });
-        assert.equal(drafts.length, 1);
-        assert.equal(drafts[0].message,
-            '> is this a crossover episode!?\n> It might be!\n\n');
-        assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
-        done();
+
+      var quoteBtn = element.$.quoteBtn;
+      MockInteractions.tap(quoteBtn);
+      flushAsynchronousOperations();
+
+      var drafts = element._orderedComments.filter(function(c) {
+        return c.__draft == true;
       });
-      commentEl.fire('create-reply-comment', {comment: commentEl.comment,
-          quote: true}, {bubbles: false});
+      assert.equal(drafts.length, 1);
+      assert.equal(drafts[0].message,
+          '> is this a crossover episode!?\n> It might be!\n\n');
+      assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+      done();
     });
 
     test('ack', function(done) {
       element.changeNum = '42';
       element.patchNum = '1';
+
       var commentEl = element.$$('gr-diff-comment');
       assert.ok(commentEl);
-      commentEl.addEventListener('create-ack-comment', function() {
-        var drafts = element._orderedComments.filter(function(c) {
+
+      var ackBtn = element.$.ackBtn;
+      MockInteractions.tap(ackBtn);
+      flush(function() {
+        var drafts = element.comments.filter(function(c) {
           return c.__draft == true;
         });
         assert.equal(drafts.length, 1);
@@ -261,8 +280,6 @@
         assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
         done();
       });
-      commentEl.fire('create-ack-comment', {comment: commentEl.comment},
-          {bubbles: false});
     });
 
     test('done', function(done) {
@@ -270,8 +287,11 @@
       element.patchNum = '1';
       var commentEl = element.$$('gr-diff-comment');
       assert.ok(commentEl);
-      commentEl.addEventListener('create-done-comment', function() {
-        var drafts = element._orderedComments.filter(function(c) {
+
+      var doneBtn = element.$.doneBtn;
+      MockInteractions.tap(doneBtn);
+      flush(function() {
+        var drafts = element.comments.filter(function(c) {
           return c.__draft == true;
         });
         assert.equal(drafts.length, 1);
@@ -280,8 +300,6 @@
         assert.isFalse(drafts[0].unresolved);
         done();
       });
-      commentEl.fire('create-done-comment', {comment: commentEl.comment},
-          {bubbles: false});
     });
 
     test('please fix', function(done) {
@@ -328,7 +346,7 @@
     });
 
     test('first editing comment does not add __otherEditing attribute',
-        function(done) {
+        function() {
       var commentEl = element.$$('gr-diff-comment');
       element.comments = [{
         author: {
@@ -341,101 +359,16 @@
         updated: '2015-12-08 19:48:33.843000000',
         __draft: true,
       }];
+
+      var replyBtn = element.$.replyBtn;
+      MockInteractions.tap(replyBtn);
       flushAsynchronousOperations();
 
-      commentEl.addEventListener('create-reply-comment', function() {
-        var editing = element._orderedComments.filter(function(c) {
-          return c.__editing == true;
-        });
-        assert.equal(editing.length, 1);
-        assert.equal(!!editing[0].__otherEditing, false);
-        done();
+      var editing = element._orderedComments.filter(function(c) {
+        return c.__editing == true;
       });
-      commentEl.fire('create-reply-comment', {comment: commentEl.comment},
-          {bubbles: false});
-    });
-
-    test('two editing comments adds __otherEditing attribute', function(done) {
-      var commentEl = element.$$('gr-diff-comment');
-      element.comments = [{
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 'baf0414d_60047215',
-        line: 5,
-        message: 'is this a crossover episode!?',
-        updated: '2015-12-08 19:48:33.843000000',
-        __editing: true,
-        __draft: true,
-      }];
-      flushAsynchronousOperations();
-
-      commentEl.addEventListener('create-reply-comment', function() {
-        var editing = element._orderedComments.filter(function(c) {
-          return c.__editing == true;
-        });
-        assert.equal(editing.length, 2);
-        assert.equal(editing[1].__otherEditing, true);
-        done();
-      });
-      commentEl.fire('create-reply-comment', {comment: commentEl.comment},
-          {bubbles: false});
-    });
-
-    test('When editing other comments, local storage set after discard',
-        function(done) {
-      element.changeNum = '42';
-      element.patchNum = '1';
-      element.comments = [{
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        id: 'baf0414d_60047215',
-        in_reply_to: 'baf0414d_60047215',
-        line: 5,
-        message: 'is this a crossover episode!?',
-        updated: '2015-12-08 19:48:31.843000000',
-      },
-      {
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        __draftID: '1',
-        in_reply_to: 'baf0414d_60047215',
-        line: 5,
-        message: 'yes',
-        updated: '2015-12-08 19:48:32.843000000',
-        __draft: true,
-        __editing: true,
-      },
-      {
-        author: {
-          name: 'Mr. Peanutbutter',
-          email: 'tenn1sballchaser@aol.com',
-        },
-        __draftID: '2',
-        in_reply_to: 'baf0414d_60047215',
-        line: 5,
-        message: 'no',
-        updated: '2015-12-08 19:48:33.843000000',
-        __draft: true,
-        __editing: true,
-      }];
-      var storageStub = sinon.stub(element.$.storage, 'setDraftComment');
-      flushAsynchronousOperations();
-
-      var draftEl =
-          Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
-      assert.ok(draftEl);
-      draftEl.addEventListener('comment-discard', function() {
-        assert.isTrue(storageStub.called);
-        storageStub.restore();
-        done();
-      });
-      draftEl.fire('comment-discard', null, {bubbles: false});
+      assert.equal(editing.length, 1);
+      assert.equal(!!editing[0].__otherEditing, false);
     });
 
     test('When not editing other comments, local storage not set after discard',
@@ -581,10 +514,37 @@
       assert.equal(element._computeHostClass(false), '');
     });
 
+    test('addDraft sets unresolved state correctly', function() {
+      var unresolved = true;
+      element.comments = [];
+      element.addDraft(null, null, unresolved);
+      assert.equal(element.comments[0].unresolved, true);
+
+      unresolved = false; // comment should get added as actually resolved.
+      element.comments = [];
+      element.addDraft(null, null, unresolved);
+      assert.equal(element.comments[0].unresolved, false);
+
+      element.comments = [];
+      element.addDraft();
+      assert.equal(element.comments[0].unresolved, true);
+    });
+
     test('_newDraft', function() {
       element.commentSide = 'left';
       var draft = element._newDraft();
       assert.equal(draft.__commentSide, 'left');
     });
+
+    test('new comment gets created', function() {
+      element.comments = [];
+      element.addOrEditDraft(1);
+      assert.equal(element.comments.length, 1);
+      // Mock a submitted comment.
+      element.comments[0].__draft = false;
+      element.comments[0].id = element.comments[0].__draftID;
+      element.addOrEditDraft(1);
+      assert.equal(element.comments.length, 2);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index ceedf73..8863aa6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -134,7 +134,7 @@
         margin-top: -.4em;
       }
       .runIdInformation {
-        margin-bottom: .5em;
+        margin: 1em 0;
       }
       .robotRun {
         margin-left: .5em;
@@ -218,6 +218,7 @@
           on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
       <gr-formatted-text class="message"
           content="[[comment.message]]"
+          no-trailing-margin="[[!comment.__draft]]"
           collapsed="[[collapsed]]"
           config="[[projectConfig.commentlinks]]"></gr-formatted-text>
       <div hidden$="[[!comment.robot_run_id]]">
@@ -229,11 +230,6 @@
         </div>
       </div>
       <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
-        <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
-        <gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button>
-        <gr-button class="action ack" on-tap="_handleAck">Ack</gr-button>
-        <gr-button class="action done" on-tap="_handleDone">
-          Done</gr-button>
         <gr-button class="action edit hideOnPublished" on-tap="_handleEdit">
             Edit</gr-button>
         <gr-button class="action save hideOnPublished" on-tap="_handleSave"
@@ -254,7 +250,9 @@
         </div>
       </div>
       <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
-        <gr-button class="action fix" on-tap="_handleFix">
+        <gr-button class="action fix"
+            on-tap="_handleFix"
+            disabled="[[robotButtonDisabled]]">
           Please Fix
         </gr-button>
       </div>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index bc12b8f..1e6eef8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -20,24 +20,6 @@
     is: 'gr-diff-comment',
 
     /**
-     * Fired when the Reply action is triggered.
-     *
-     * @event create-reply-comment
-     */
-
-    /**
-     * Fired when the Ack action is triggered.
-     *
-     * @event create-ack-comment
-     */
-
-    /**
-     * Fired when the Done action is triggered.
-     *
-     * @event create-done-comment
-     */
-
-    /**
      * Fired when the create fix comment action is triggered.
      *
      * @event create-fix-comment
@@ -107,6 +89,7 @@
         observer: '_toggleCollapseClass',
       },
       projectConfig: Object,
+      robotButtonDisabled: Boolean,
 
       _xhrPromise: Object,  // Used for testing.
       _messageText: {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 6ea5b96..1196c1b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -98,37 +98,6 @@
           'header middle content is not visible');
     });
 
-    test('proper event fires on reply', function(done) {
-      element.addEventListener('create-reply-comment', function(e) {
-        assert.ok(e.detail.comment);
-        done();
-      });
-      MockInteractions.tap(element.$$('.reply'));
-    });
-
-    test('proper event fires on quote', function(done) {
-      element.addEventListener('create-reply-comment', function(e) {
-        assert.ok(e.detail.comment);
-        assert.isTrue(e.detail.quote);
-        done();
-      });
-      MockInteractions.tap(element.$$('.quote'));
-    });
-
-    test('proper event fires on ack', function(done) {
-      element.addEventListener('create-ack-comment', function(e) {
-        done();
-      });
-      MockInteractions.tap(element.$$('.ack'));
-    });
-
-    test('proper event fires on done', function(done) {
-      element.addEventListener('create-done-comment', function(e) {
-        done();
-      });
-      MockInteractions.tap(element.$$('.done'));
-    });
-
     test('clicking on date link does not trigger nav', function() {
       var showStub = sinon.stub(page, 'show');
       var dateEl = element.$$('.date');
@@ -286,10 +255,6 @@
       assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
       assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
       assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
-      assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible');
-      assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible');
-      assert.isFalse(isVisible(element.$$('.ack')), 'ack is not visible');
-      assert.isFalse(isVisible(element.$$('.done')), 'done is not visible');
       assert.isFalse(isVisible(element.$$('.resolve')),
           'resolve is not visible');
       assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
@@ -300,10 +265,6 @@
       assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
       assert.isTrue(isVisible(element.$$('.save')), 'save is visible');
       assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is visible');
-      assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible');
-      assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible');
-      assert.isFalse(isVisible(element.$$('.ack')), 'ack is not visible');
-      assert.isFalse(isVisible(element.$$('.done')), 'done is not visible');
       assert.isTrue(isVisible(element.$$('.resolve')), 'resolve is visible');
       assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
       assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
@@ -315,10 +276,6 @@
           'discard is not visible');
       assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
       assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
-      assert.isTrue(isVisible(element.$$('.reply')), 'reply is visible');
-      assert.isTrue(isVisible(element.$$('.quote')), 'quote is visible');
-      assert.isTrue(isVisible(element.$$('.ack')), 'ack is visible');
-      assert.isTrue(isVisible(element.$$('.done')), 'done is visible');
       assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
       assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 6221481..a5147f1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -230,7 +230,7 @@
       var threadEl = this._getOrCreateThreadAtLineRange(contentEl, patchNum,
           diffSide, side, range);
 
-      threadEl.addDraft(line, range);
+      threadEl.addOrEditDraft(line, range);
     },
 
     _addDraft: function(lineEl, opt_lineNum) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index c72e3da..db07de4 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -62,12 +62,16 @@
         display: block;
         padding: .85em 1em;
       }
+      li .itemAction.disabled {
+        color: #ccc;
+        cursor: default;
+      }
       li .itemAction:link,
       li .itemAction:visited {
         color: #00e;
         text-decoration: none;
       }
-      li .itemAction:hover {
+      li .itemAction:not(.disabled):hover {
         background-color: #6B82D6;
         color: #fff;
       }
@@ -127,7 +131,7 @@
               initial-count="75">
             <li>
               <span
-                  class="itemAction"
+                  class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]"
                   data-id$="[[link.id]]"
                   on-tap="_handleItemTap"
                   hidden$="[[link.url]]">[[link.name]]</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index d1fae7f..6ab1301 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -44,6 +44,15 @@
         value: 40,
       },
 
+      /**
+       * List the IDs of dropdown buttons to be disabled. (Note this only
+       * diisables bittons and not link entries.)
+       */
+      disabledIds: {
+        type: Array,
+        value: function() { return []; },
+      },
+
       _hasAvatars: String,
     },
 
@@ -76,7 +85,13 @@
 
     _handleItemTap: function(e) {
       var id = e.target.getAttribute('data-id');
-      if (id) { this.dispatchEvent(new CustomEvent('tap-item-' + id)); }
+      if (id && this.disabledIds.indexOf(id) === -1) {
+        this.dispatchEvent(new CustomEvent('tap-item-' + id));
+      }
+    },
+
+    _computeDisabledClass: function(id, disabledIdsRecord) {
+      return disabledIdsRecord.base.indexOf(id) === -1 ? '' : 'disabled';
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 2794caf..390213c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -80,5 +80,16 @@
       MockInteractions.tap(element.$$('.itemAction'));
       assert.isTrue(stub.called);
     });
+
+    test('disabled non link item', function() {
+      element.items = [{name: 'item one', id: 'foo'}];
+      element.disabledIds = ['foo'];
+
+      var stub = sinon.stub();
+      element.addEventListener('tap-item-foo', stub);
+      flushAsynchronousOperations();
+      MockInteractions.tap(element.$$('.itemAction'));
+      assert.isFalse(stub.called);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 77e2272..86211a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -44,6 +44,7 @@
         type: Boolean,
         value: false,
       },
+      removeZeroWidthSpace: Boolean,
       _saveDisabled: {
         computed: '_computeSaveDisabled(disabled, content, _newContent)',
         type: Boolean,
@@ -58,7 +59,10 @@
 
     _editingChanged: function(editing) {
       if (!editing) { return; }
-      this._newContent = this.content;
+
+      // TODO(wyatta) switch linkify sequence, see issue 5526.
+      this._newContent = this.removeZeroWidthSpace ?
+          this.content.replace(/^R=\u200B/gm, 'R=') : this.content;
     },
 
     _computeSaveDisabled: function(disabled, content, newContent) {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
index 999f171..2515fe1 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -69,6 +69,13 @@
       assert.equal(element._newContent, 'stale content');
     });
 
+    test('zero width spaces are removed properly', function() {
+      element.removeZeroWidthSpace = true;
+      element.content = 'R=\u200Btest@google.com';
+      element.editing = true;
+      assert.equal(element._newContent, 'R=test@google.com');
+    });
+
     suite('editing', function() {
       setup(function() {
         element.content = 'current content';
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
index 8855b0a..d719f70 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
@@ -45,6 +45,7 @@
       gr-linked-text.pre {
         font-family: var(--monospace-font-family);
       }
+
     </style>
     <div id="container"></div>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index f4d99d6..e7ca50d 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -129,6 +129,7 @@
 };
 
 GrLinkTextParser.prototype.parseChunk = function(text, href) {
+  // TODO(wyatta) switch linkify sequence, see issue 5526.
   if (this.removeZeroWidthSpace) {
     // Remove the zero-width space added in gr-change-view.
     text = text.replace(/^R=\u200B/gm, 'R=');
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 33f33b7..b19137e 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -33,6 +33,7 @@
 	port     = flag.String("port", ":8081", "Port to serve HTTP requests on")
 	prod     = flag.Bool("prod", false, "Serve production assets")
 	scheme   = flag.String("scheme", "https", "URL scheme")
+	plugins  = flag.String("plugins", "", "Path to local plugins folder")
 )
 
 func main() {
@@ -49,6 +50,11 @@
 	http.HandleFunc("/config/", handleRESTProxy)
 	http.HandleFunc("/projects/", handleRESTProxy)
 	http.HandleFunc("/accounts/self/detail", handleAccountDetail)
+	if len(*plugins) > 0 {
+		http.Handle("/plugins/", http.StripPrefix("/plugins/",
+			http.FileServer(http.Dir(*plugins))))
+		log.Println("Local plugins at", *plugins)
+	}
 	log.Println("Serving on port", *port)
 	log.Fatal(http.ListenAndServe(*port, &server{}))
 }
diff --git a/tools/gerrit.importorder b/tools/gerrit.importorder
deleted file mode 100644
index 398130e..0000000
--- a/tools/gerrit.importorder
+++ /dev/null
@@ -1,12 +0,0 @@
-#Organize Import Order
-#Mon Mar 23 17:27:34 PDT 2015
-9=javax
-8=java
-7=org
-6=net
-5=junit
-4=eu
-3=dk
-2=com
-1=com.google
-0=\#
diff --git a/tools/maven/BUILD b/tools/maven/BUILD
index 9ac46ac..d46a954 100644
--- a/tools/maven/BUILD
+++ b/tools/maven/BUILD
@@ -3,8 +3,7 @@
 
 MAVEN_REPOSITORY = "sonatype-nexus-staging"
 
-# TODO(davido): support snapshot repositories
-MAVEN_RELEASE_URL = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
+URL = "https://oss.sonatype.org/content/repositories/snapshots" if GERRIT_VERSION.endswith("-SNAPSHOT") else "https://oss.sonatype.org/service/local/staging/deploy/maven2"
 
 maven_package(
     src = {
@@ -26,7 +25,7 @@
         "gerrit-plugin-gwtui": "//gerrit-plugin-gwtui:gwtui-api_deploy.jar",
     },
     repository = MAVEN_REPOSITORY,
-    url = MAVEN_RELEASE_URL,
+    url = URL,
     version = GERRIT_VERSION,
     war = {"gerrit-war": "//:release"},
 )
diff --git a/tools/setup_gjf.sh b/tools/setup_gjf.sh
index 443f1ee..0cf2f3f 100755
--- a/tools/setup_gjf.sh
+++ b/tools/setup_gjf.sh
@@ -17,8 +17,8 @@
 set -eu
 
 # Keep this version in sync with dev-contributing.txt.
-VERSION="1.2"
-SHA1="b3614824e178dd918dc2cc7c7b54804200ebfc64"
+VERSION="1.3"
+SHA1="a73cfe6f9af01bd6ff150c0b50c9d620400f784c"
 
 root="$(git rev-parse --show-toplevel)"
 if [[ -z "$root" ]]; then