Merge "Support to query groups by owner group name"
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 d7882a5..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
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/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 6a78d9f..b0e64a8 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5152,6 +5152,8 @@
Number of inserted lines.
|`deletions` ||
Number of deleted lines.
+|`unresolved_comment_count` |optional|
+Number of unresolved comments. Not set if the current change index doesn't have the data.
|`_number` ||The legacy numeric ID of the change.
|`owner` ||
The owner of the change as an link:rest-api-accounts.html#account-info[
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/Documentation/user-search.txt b/Documentation/user-search.txt
index f1f1654..2b5702e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -278,6 +278,10 @@
+
True if the change has inline edit created by the current user.
+has:unresolved::
++
+True if the change has unresolved comments.
+
[[is]]
[[is-starred]]
is:starred::
@@ -417,6 +421,16 @@
only applies to the top-level status; individual label statuses can be
searched link:#labels[by label].
+[[unresolved]]
+unresolved:'RELATION''NUMBER'::
++
+True if the number of unresolved comments satisfies the given relation for the given number.
++
+For example, unresolved:>0 will be true for any change which has at least one unresolved
+comment while unresolved:0 will be true for any change which has all comments resolved.
++
+Valid relations are >=, >, <=, <, or no relation, which will match if the number of unresolved
+comments is exactly equal.
== Argument Quoting
diff --git a/WORKSPACE b/WORKSPACE
index 986b4c2..1ffe850 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -696,10 +696,18 @@
sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
)
+TRUTH_VERS = "0.31"
+
maven_jar(
name = "truth",
- artifact = "com.google.truth:truth:0.30",
- sha1 = "9d591b5a66eda81f0b88cf1c748ab8853d99b18b",
+ artifact = "com.google.truth:truth:" + TRUTH_VERS,
+ sha1 = "1a926b0cb2879fd32efbb3716ee8bab040f4218b",
+)
+
+maven_jar(
+ name = "truth-java8-extension",
+ artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
+ sha1 = "a7e80e631f2bf4ecc2b99ad1e33059eb0dcc6ea0",
)
maven_jar(
@@ -1098,8 +1106,8 @@
bower_archive(
name = "web-component-tester",
package = "web-component-tester",
- sha1 = "54556000c33d9ed7949aa546c1b4a1531491a5f0",
- version = "4.2.2",
+ sha1 = "a4a9bc7815a22d143e8f8593e37b3c2028b8c20f",
+ version = "5.0.0",
)
# Bower component transitive dependencies.
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD
index 69f132b..db5a300 100644
--- a/gerrit-acceptance-framework/BUILD
+++ b/gerrit-acceptance-framework/BUILD
@@ -41,6 +41,7 @@
"//gerrit-server:testutil",
"//gerrit-server/src/main/prolog:common",
"//lib:truth",
+ "//lib:truth-java8-extension",
"//lib/auto:auto-value",
"//lib/httpcomponents:fluent-hc",
"//lib/httpcomponents:httpclient",
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/BUILD b/gerrit-acceptance-tests/BUILD
index e7a6222..3154b1f 100644
--- a/gerrit-acceptance-tests/BUILD
+++ b/gerrit-acceptance-tests/BUILD
@@ -1,5 +1,6 @@
java_library(
name = "lib",
+ srcs = ["src/test/java/com/google/gerrit/acceptance/Dummy.java"],
testonly = 1,
visibility = ["//visibility:public"],
exports = [
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/Dummy.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/Dummy.java
new file mode 100644
index 0000000..d910638
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/Dummy.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+public class Dummy {
+}
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/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 998abbf..df82e21 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.api.change;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
@@ -31,9 +32,11 @@
import static com.google.gerrit.server.project.Util.value;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -57,6 +60,7 @@
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeKind;
@@ -114,6 +118,8 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
@@ -945,17 +951,41 @@
}
@Test
- public void implicitlyCcOnNonVotingReview() throws Exception {
+ public void implicitlyCcOnNonVotingReviewPgStyle() throws Exception {
PushOneCommit.Result r = createChange();
setApiUser(user);
- gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(new ReviewInput());
+ assertThat(getReviewerState(r.getChangeId(), user.id)).isEmpty();
- ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
- // If we're not reading from NoteDb, then the CCed user will be returned
- // in the REVIEWER state.
- ReviewerState state = notesMigration.readChanges() ? CC : REVIEWER;
- assertThat(c.reviewers.get(state).stream().map(ai -> ai._accountId).collect(toList()))
- .containsExactly(user.id.get());
+ // Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
+ ReviewInput in = new ReviewInput();
+ in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+ in.labels = ImmutableMap.of();
+ in.message = "comment";
+ in.reviewers = ImmutableList.of();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
+
+ // If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state.
+ assertThat(getReviewerState(r.getChangeId(), user.id))
+ .hasValue(notesMigration.readChanges() ? CC : REVIEWER);
+ }
+
+ @Test
+ public void implicitlyCcOnNonVotingReviewGwtStyle() throws Exception {
+ PushOneCommit.Result r = createChange();
+ setApiUser(user);
+ assertThat(getReviewerState(r.getChangeId(), user.id)).isEmpty();
+
+ // Exact request format made by GWT UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
+ ReviewInput in = new ReviewInput();
+ in.labels = ImmutableMap.of("Code-Review", (short) 0);
+ in.strictLabels = true;
+ in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+ in.message = "comment";
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
+
+ // If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state.
+ assertThat(getReviewerState(r.getChangeId(), user.id))
+ .hasValue(notesMigration.readChanges() ? CC : REVIEWER);
}
@Test
@@ -2290,6 +2320,20 @@
return changeResourceFactory.create(ctls.get(0));
}
+ private Optional<ReviewerState> getReviewerState(String changeId, Account.Id accountId)
+ throws Exception {
+ ChangeInfo c = gApi.changes().id(changeId).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+ Set<ReviewerState> states =
+ c.reviewers
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue().stream().anyMatch(a -> a._accountId == accountId.get()))
+ .map(e -> e.getKey())
+ .collect(toSet());
+ assertThat(states.size()).named(states.toString()).isAtMost(1);
+ return states.stream().findFirst();
+ }
+
private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
try (BatchUpdate batchUpdate =
updateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 283f14b..faa21cf 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -17,14 +17,17 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.extensions.common.RobotCommentInfoSubject.assertThatList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.FixReplacementInfo;
import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
@@ -364,6 +367,31 @@
gApi.changes().id(changeId).current().review(reviewInput);
}
+ @Test
+ public void queryChangesWithUnresolvedCommentCount() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 =
+ pushFactory
+ .create(
+ db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
+ .to("refs/for/master");
+
+ addRobotComment(r2.getChangeId(), createRobotCommentInputWithMandatoryFields());
+
+ AcceptanceTestRequestScope.Context ctx = disableDb();
+ try {
+ ChangeInfo result = Iterables.getOnlyElement(query(r2.getChangeId()));
+ // currently, we create all robot comments as 'resolved' by default.
+ // if we allow users to resolve a robot comment, then this test should
+ // be modified.
+ assertThat(result.unresolvedCommentCount).isEqualTo(0);
+ } finally {
+ enableDb(ctx);
+ }
+ }
+
private RobotCommentInput createRobotCommentInputWithMandatoryFields() {
RobotCommentInput in = new RobotCommentInput();
in.robotId = "happyRobot";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 72146ab..a4b2209 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -866,6 +866,16 @@
@Test
public void pushAFewChanges() throws Exception {
+ testPushAFewChanges();
+ }
+
+ @Test
+ public void pushAFewChangesWithCreateNewChangeForAllNotInTarget() throws Exception {
+ enableCreateNewChangeForAllNotInTarget();
+ testPushAFewChanges();
+ }
+
+ private void testPushAFewChanges() throws Exception {
int n = 10;
String r = "refs/for/master";
ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
@@ -991,11 +1001,75 @@
pushForReviewRejected(testRepo, "invalid Change-Id line format in commit message footer");
}
+ @Test
+ public void pushCommitWithSameChangeIdAsPredecessorChange() throws Exception {
+ PushOneCommit push =
+ pushFactory.create(
+ db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ RevCommit commitChange1 = r.getCommit();
+
+ createCommit(testRepo, commitChange1.getFullMessage());
+
+ pushForReviewRejected(
+ testRepo,
+ "same Change-Id in multiple changes.\n"
+ + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+ + " commit");
+
+ ProjectConfig config = projectCache.checkedGet(project).getConfig();
+ config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+ saveProjectConfig(project, config);
+
+ pushForReviewRejected(
+ testRepo,
+ "same Change-Id in multiple changes.\n"
+ + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+ + " commit");
+ }
+
+ @Test
+ public void pushTwoCommitWithSameChangeId() throws Exception {
+ RevCommit commitChange1 = createCommitWithChangeId(testRepo, "some change");
+
+ createCommit(testRepo, commitChange1.getFullMessage());
+
+ pushForReviewRejected(
+ testRepo,
+ "same Change-Id in multiple changes.\n"
+ + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+ + " commit");
+
+ ProjectConfig config = projectCache.checkedGet(project).getConfig();
+ config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+ saveProjectConfig(project, config);
+
+ pushForReviewRejected(
+ testRepo,
+ "same Change-Id in multiple changes.\n"
+ + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+ + " commit");
+ }
+
private static RevCommit createCommit(TestRepository<?> testRepo, String message)
throws Exception {
return testRepo.branch("HEAD").commit().message(message).add("a.txt", "content").create();
}
+ private static RevCommit createCommitWithChangeId(TestRepository<?> testRepo, String message)
+ throws Exception {
+ RevCommit c =
+ testRepo
+ .branch("HEAD")
+ .commit()
+ .message(message)
+ .insertChangeId()
+ .add("a.txt", "content")
+ .create();
+ return testRepo.getRevWalk().parseCommit(c);
+ }
+
@Test
public void cantAutoCloseChangeAlreadyMergedToBranch() throws Exception {
PushOneCommit.Result r1 = createChange();
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/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 2fa0f4d..7a84e6d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -23,6 +23,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.extensions.api.changes.DraftInput;
@@ -31,6 +32,7 @@
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
@@ -401,7 +403,7 @@
addComment(r1, "nit: trailing whitespace");
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result.get(FILE_NAME)).hasSize(2);
- addComment(r1, "nit: trailing whitespace", true);
+ addComment(r1, "nit: trailing whitespace", true, false);
result = getPublishedComments(changeId, revId);
assertThat(result.get(FILE_NAME)).hasSize(2);
@@ -411,7 +413,7 @@
.to("refs/for/master");
changeId = r2.getChangeId();
revId = r2.getCommit().getName();
- addComment(r2, "nit: trailing whitespace", true);
+ addComment(r2, "nit: trailing whitespace", true, false);
result = getPublishedComments(changeId, revId);
assertThat(result.get(FILE_NAME)).hasSize(1);
}
@@ -694,6 +696,30 @@
assertThat(drafts.get(0).tag).isEqualTo("tag2");
}
+ @Test
+ public void queryChangesWithUnresolvedCommentCount() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+
+ addComment(r1, "comment 1", false, true);
+ addComment(r1, "nit: trailing whitespace", false, null);
+
+ PushOneCommit.Result r2 =
+ pushFactory
+ .create(
+ db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
+ .to("refs/for/master");
+
+ addComment(r2, "typo: content", false, false);
+
+ AcceptanceTestRequestScope.Context ctx = disableDb();
+ try {
+ ChangeInfo result = Iterables.getOnlyElement(query(r2.getChangeId()));
+ assertThat(result.unresolvedCommentCount).isEqualTo(1);
+ } finally {
+ enableDb(ctx);
+ }
+ }
+
private static String extractComments(String msg) {
// Extract lines between start "....." and end "-- ".
Pattern p = Pattern.compile(".*[.]{5}\n+(.*)\\n+-- \n.*", Pattern.DOTALL);
@@ -709,15 +735,17 @@
}
private void addComment(PushOneCommit.Result r, String message) throws Exception {
- addComment(r, message, false);
+ addComment(r, message, false, false);
}
- private void addComment(PushOneCommit.Result r, String message, boolean omitDuplicateComments)
+ private void addComment(
+ PushOneCommit.Result r, String message, boolean omitDuplicateComments, Boolean unresolved)
throws Exception {
CommentInput c = new CommentInput();
c.line = 1;
c.message = message;
c.path = FILE_NAME;
+ c.unresolved = unresolved;
ReviewInput in = newInput(c);
in.omitDuplicateComments = omitDuplicateComments;
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index dc2668d..83bb3af 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -353,6 +353,7 @@
ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT,
cd);
+ decodeUnresolvedCommentCount(source, ChangeField.UNRESOLVED_COMMENT_COUNT.getName(), cd);
if (source.get(ChangeField.REF_STATE.getName()) != null) {
JsonArray refStates = source.get(ChangeField.REF_STATE.getName()).getAsJsonArray();
@@ -381,5 +382,13 @@
opts,
out);
}
+
+ private void decodeUnresolvedCommentCount(JsonObject doc, String fieldName, ChangeData out) {
+ JsonElement count = doc.get(fieldName);
+ if (count == null) {
+ return;
+ }
+ out.setUnresolvedCommentCount(count.getAsInt());
+ }
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 7061f31..3803714 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -43,6 +43,7 @@
public Boolean submittable;
public Integer insertions;
public Integer deletions;
+ public Integer unresolvedCommentCount;
public int _number;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index cbd12ea..165d0ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -118,6 +118,7 @@
suggestions.add("has:edit");
suggestions.add("has:star");
suggestions.add("has:stars");
+ suggestions.add("has:unresolved");
suggestions.add("star:");
suggestions.add("is:");
@@ -148,6 +149,8 @@
suggestions.add("delta:");
suggestions.add("size:");
+ suggestions.add("unresolved:");
+
if (Gerrit.isNoteDbEnabled()) {
suggestions.add("hashtag:");
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdInfo.java
index 8d5cd68..4ac0716 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ExternalIdInfo.java
@@ -57,8 +57,6 @@
// Describe a mailto address as just its email address,
// which is already shown in the email address field.
return "";
- } else if (isScheme("https://www.google.com/accounts/o8/id")) {
- return OpenIdUtil.C.nameGoogle();
} else if (isScheme(OpenIdUrls.URL_LAUNCHPAD)) {
return OpenIdUtil.C.nameLaunchpad();
} else if (isScheme(OpenIdUrls.URL_YAHOO)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
index 2c21b74..a0eaef7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
@@ -17,8 +17,6 @@
import com.google.gwt.i18n.client.Constants;
public interface OpenIdConstants extends Constants {
- String nameGoogle();
-
String nameLaunchpad();
String nameYahoo();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
index 08ddf38..d6e8de6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
@@ -1,3 +1,2 @@
-nameGoogle = Google Account
nameLaunchpad = Launchpad ID
nameYahoo = Yahoo! ID
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 5477b31..ba5780e 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -127,6 +127,8 @@
ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName();
private static final String SUBMIT_RECORD_STRICT_FIELD =
ChangeField.STORED_SUBMIT_RECORD_STRICT.getName();
+ private static final String UNRESOLVED_COMMENT_COUNT_FIELD =
+ ChangeField.UNRESOLVED_COMMENT_COUNT.getName();
static Term idTerm(ChangeData cd) {
return QueryBuilder.intTerm(LEGACY_ID.getName(), cd.getId().get());
@@ -467,6 +469,8 @@
if (fields.contains(REF_STATE_PATTERN_FIELD)) {
decodeRefStatePatterns(doc, cd);
}
+
+ decodeUnresolvedCommentCount(doc, cd);
return cd;
}
@@ -568,6 +572,14 @@
cd.setRefStatePatterns(copyAsBytes(doc.get(REF_STATE_PATTERN_FIELD)));
}
+ private void decodeUnresolvedCommentCount(
+ ListMultimap<String, IndexableField> doc, ChangeData cd) {
+ IndexableField f = Iterables.getFirst(doc.get(UNRESOLVED_COMMENT_COUNT_FIELD), null);
+ if (f != null && f.numericValue() != null) {
+ cd.setUnresolvedCommentCount(f.numericValue().intValue());
+ }
+ }
+
private static <T> List<T> decodeProtos(
ListMultimap<String, IndexableField> doc, String fieldName, ProtobufCodec<T> codec) {
Collection<IndexableField> fields = doc.get(fieldName);
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/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 8e1ab10..3b765ef 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
@@ -477,9 +477,10 @@
out.created = in.getCreatedOn();
out.updated = in.getLastUpdatedOn();
out._number = in.getId().get();
+ out.unresolvedCommentCount = cd.unresolvedCommentCount();
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/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 790c241..d0c0e29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -244,7 +244,10 @@
batchUpdateFactory.create(
db.get(), revision.getChange().getProject(), revision.getUser(), ts)) {
Account.Id id = bu.getUser().getAccountId();
- boolean ccOrReviewer = input.labels != null && !input.labels.isEmpty();
+ boolean ccOrReviewer = false;
+ if (input.labels != null && !input.labels.isEmpty()) {
+ ccOrReviewer = input.labels.values().stream().filter(v -> v != 0).findFirst().isPresent();
+ }
if (!ccOrReviewer) {
// Check if user was already CCed or reviewing prior to this review.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 4747e1a..e2ca3dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -547,6 +547,16 @@
}
};
+ /** Number of unresolved comments of the change. */
+ public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
+ new FieldDef.Single<ChangeData, Integer>(
+ ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT, FieldType.INTEGER_RANGE, true) {
+ @Override
+ public Integer get(ChangeData input, FillArgs args) throws OrmException {
+ return input.unresolvedCommentCount();
+ }
+ };
+
/** Whether the change is mergeable. */
public static final FieldDef<ChangeData, String> MERGEABLE =
new FieldDef.Single<ChangeData, String>(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 76a9bc9..a00dfe2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -85,7 +85,9 @@
static final Schema<ChangeData> V36 =
schema(V35, ChangeField.REF_STATE, ChangeField.REF_STATE_PATTERN);
- static final Schema<ChangeData> V37 = schema(V36);
+ @Deprecated static final Schema<ChangeData> V37 = schema(V36);
+
+ static final Schema<ChangeData> V38 = schema(V37, ChangeField.UNRESOLVED_COMMENT_COUNT);
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
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..f282c2d 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/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..84af7be 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;
@@ -39,6 +41,7 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RobotComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.ApprovalsUtil;
@@ -81,6 +84,7 @@
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -332,6 +336,7 @@
private Map<Integer, List<String>> files;
private Map<Integer, Optional<DiffSummary>> diffSummaries;
private Collection<Comment> publishedComments;
+ private Collection<RobotComment> robotComments;
private CurrentUser visibleTo;
private ChangeControl changeControl;
private List<ChangeMessage> messages;
@@ -342,13 +347,14 @@
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;
private PersonIdent author;
private PersonIdent committer;
+ private Integer unresolvedCommentCount;
private ImmutableList<byte[]> refStates;
private ImmutableList<byte[]> refStatePatterns;
@@ -975,6 +981,34 @@
return publishedComments;
}
+ public Collection<RobotComment> robotComments() throws OrmException {
+ if (robotComments == null) {
+ if (!lazyLoad) {
+ return Collections.emptyList();
+ }
+ robotComments = commentsUtil.robotCommentsByChange(notes());
+ }
+ return robotComments;
+ }
+
+ public Integer unresolvedCommentCount() throws OrmException {
+ if (unresolvedCommentCount == null) {
+ if (!lazyLoad) {
+ return null;
+ }
+ Long count =
+ Stream.concat(publishedComments().stream(), robotComments().stream())
+ .filter(c -> (c.unresolved == Boolean.TRUE))
+ .count();
+ unresolvedCommentCount = count.intValue();
+ }
+ return unresolvedCommentCount;
+ }
+
+ public void setUnresolvedCommentCount(Integer count) {
+ this.unresolvedCommentCount = count;
+ }
+
public List<ChangeMessage> messages() throws OrmException {
if (messages == null) {
if (!lazyLoad) {
@@ -1180,23 +1214,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 +1242,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 +1297,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/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 2af5cd8..aa220e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -159,6 +159,7 @@
public static final String FIELD_STATUS = "status";
public static final String FIELD_SUBMISSIONID = "submissionid";
public static final String FIELD_TR = "tr";
+ public static final String FIELD_UNRESOLVED_COMMENT_COUNT = "unresolved";
public static final String FIELD_VISIBLETO = "visibleto";
public static final String FIELD_WATCHEDBY = "watchedby";
@@ -513,6 +514,10 @@
return new EditByPredicate(self());
}
+ if ("unresolved".equalsIgnoreCase(value)) {
+ return new IsUnresolvedPredicate();
+ }
+
// for plugins the value will be operandName_pluginName
String[] names = value.split("_");
if (names.length == 2) {
@@ -677,7 +682,7 @@
// label:CodeReview=1,jsmith or
// label:CodeReview=1,group=android_approvers or
// label:CodeReview=1,android_approvers
- // user/groups without a label will first attempt to match user
+ // user/groups without a label will first attempt to match user
// Special case: votes by owners can be tracked with ",owner":
// label:Code-Review+2,owner
// label:Code-Review+2,user=owner
@@ -1056,6 +1061,11 @@
return new SubmittablePredicate(status);
}
+ @Operator
+ public Predicate<ChangeData> unresolved(String value) throws QueryParseException {
+ return new IsUnresolvedPredicate(value);
+ }
+
@Override
protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
if (query.startsWith("refs/")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
new file mode 100644
index 0000000..17a6347
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+
+public class IsUnresolvedPredicate extends IntegerRangeChangePredicate {
+ IsUnresolvedPredicate() throws QueryParseException {
+ this(">0");
+ }
+
+ IsUnresolvedPredicate(String value) throws QueryParseException {
+ super(ChangeField.UNRESOLVED_COMMENT_COUNT, value);
+ }
+
+ @Override
+ protected Integer getValueInt(ChangeData changeData) throws OrmException {
+ return ChangeField.UNRESOLVED_COMMENT_COUNT.get(changeData, null);
+ }
+}
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. ",
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/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index bf3e618..1b9fc61 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1549,6 +1549,37 @@
}
@Test
+ public void byUnresolved() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+ Change change3 = insert(repo, newChange(repo));
+
+ // Change1 has one resolved comment (unresolvedcount = 0)
+ // Change2 has one unresolved comment (unresolvedcount = 1)
+ // Change3 has one resolved comment and one unresolved comment (unresolvedcount = 1)
+ addComment(change1.getChangeId(), "comment 1", false);
+ addComment(change2.getChangeId(), "comment 2", true);
+ addComment(change3.getChangeId(), "comment 3", false);
+ addComment(change3.getChangeId(), "comment 4", true);
+
+ assertQuery("has:unresolved", change3, change2);
+
+ assertQuery("unresolved:0", change1);
+ List<ChangeInfo> changeInfos = assertQuery("unresolved:>=0", change3, change2, change1);
+ assertThat(changeInfos.get(0).unresolvedCommentCount).isEqualTo(1); // Change3
+ assertThat(changeInfos.get(1).unresolvedCommentCount).isEqualTo(1); // Change2
+ assertThat(changeInfos.get(2).unresolvedCommentCount).isEqualTo(0); // Change1
+ assertQuery("unresolved:>0", change3, change2);
+
+ assertQuery("unresolved:<1", change1);
+ assertQuery("unresolved:<=1", change3, change2, change1);
+ assertQuery("unresolved:1", change3, change2);
+ assertQuery("unresolved:>1");
+ assertQuery("unresolved:>=1", change3, change2);
+ }
+
+ @Test
public void byCommitsOnBranchNotMerged() throws Exception {
TestRepository<Repo> repo = createProject("repo");
int n = 10;
@@ -1595,6 +1626,7 @@
cd.changedLines();
cd.reviewedBy();
cd.reviewers();
+ cd.unresolvedCommentCount();
// TODO(dborowitz): Swap out GitRepositoryManager somehow? Will probably be
// necessary for NoteDb anyway.
@@ -1932,4 +1964,16 @@
protected static long lastUpdatedMs(Change c) {
return c.getLastUpdatedOn().getTime();
}
+
+ private void addComment(int changeId, String message, Boolean unresolved) throws Exception {
+ ReviewInput input = new ReviewInput();
+ ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
+ comment.line = 1;
+ comment.message = message;
+ comment.unresolved = unresolved;
+ input.comments =
+ ImmutableMap.<String, List<ReviewInput.CommentInput>>of(
+ Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput>of(comment));
+ gApi.changes().id(changeId).current().review(input);
+ }
}
diff --git a/lib/BUILD b/lib/BUILD
index ca0fec3..fe1933c 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -231,6 +231,17 @@
)
java_library(
+ name = "truth-java8-extension",
+ data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+ visibility = ["//visibility:public"],
+ exports = [
+ ":guava",
+ ":truth",
+ "@truth-java8-extension//jar",
+ ],
+)
+
+java_library(
name = "javassist",
data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
visibility = ["//visibility:public"],
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index aaa8b81..67cc7c0 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -75,8 +75,8 @@
bower_archive(
name = "mocha",
package = "mocha",
- version = "2.5.3",
- sha1 = "22ef0d1f43ba5e2241369c501ac648f00c0440c0")
+ version = "3.2.0",
+ sha1 = "b77f23f7ad1f1363501bcae96f0f4f47745dad0f")
bower_archive(
name = "neon-animation",
package = "neon-animation",
@@ -105,5 +105,5 @@
bower_archive(
name = "webcomponentsjs",
package = "webcomponentsjs",
- version = "0.7.22",
- sha1 = "8ba97a4a279ec6973a19b171c462a7b5cf454fb9")
+ version = "0.7.23",
+ sha1 = "3d62269e614175573b0a0f3039aab05d40f0a763")
diff --git a/polygerrit-ui/app/.gitignore b/polygerrit-ui/app/.gitignore
new file mode 100644
index 0000000..375a75d
--- /dev/null
+++ b/polygerrit-ui/app/.gitignore
@@ -0,0 +1 @@
+/plugins/
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index b094e1c..7b0eeb9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -69,11 +69,11 @@
});
suite('test show change number preference enabled', function() {
- return setup(function() {
- return stubRestAPI({legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [],
- }).then(function() {
+ setup(function() {
+ return stubRestAPI({legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [],
+ }).then(function() {
element = fixture('basic');
return element._loadPreferences();
});
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 0a7512e..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
@@ -1026,7 +1026,7 @@
_computeCommitClass: function(collapsed, commitMessage) {
if (this._computeCommitToggleHidden(commitMessage)) { return ''; }
- return collapsed ? 'commitCollapsed' : '';
+ return collapsed ? 'collapsed' : '';
},
_computeCollapseCommitText: function(collapsed) {
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 75201e4..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
@@ -964,11 +964,11 @@
element._latestCommitMessage = _.times(31, String).join('\n');
assert.isTrue(element._commitCollapsed);
assert.isTrue(
- element.$.commitMessage.classList.contains('commitCollapsed'));
+ element.$.commitMessage.classList.contains('collapsed'));
MockInteractions.tap(element.$.commitCollapseToggleButton);
assert.isFalse(element._commitCollapsed);
assert.isFalse(
- element.$.commitMessage.classList.contains('commitCollapsed'));
+ element.$.commitMessage.classList.contains('collapsed'));
});
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index c27c57e..d62dbf8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -340,7 +340,7 @@
var threadGroupEl =
document.createElement('gr-diff-comment-thread-group');
threadGroupEl.changeNum = changeNum;
- threadGroupEl.patchNum = patchNum;
+ threadGroupEl.patchForNewThreads = patchNum;
threadGroupEl.path = path;
threadGroupEl.side = side;
threadGroupEl.projectConfig = projectConfig;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 4665bdf..e5fe1d7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -241,7 +241,7 @@
function checkThreadGroupProps(threadGroupEl, patchNum, side, comments) {
assert.equal(threadGroupEl.changeNum, '42');
- assert.equal(threadGroupEl.patchNum, patchNum);
+ assert.equal(threadGroupEl.patchForNewThreads, patchNum);
assert.equal(threadGroupEl.path, '/path/to/foo');
assert.equal(threadGroupEl.side, side);
assert.deepEqual(threadGroupEl.projectConfig, {foo: 'bar'});
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..95de61f 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,15 +24,18 @@
display: block;
white-space: normal;
}
+ gr-diff-comment-thread + gr-diff-comment-thread {
+ margin-top: .2em;
+ }
</style>
- <template is="dom-repeat" items="[[_threadGroups]]"
+ <template is="dom-repeat" items="[[_threads]]"
as="thread">
<gr-diff-comment-thread
comments="[[thread.comments]]"
comment-side="[[thread.commentSide]]"
change-num="[[changeNum]]"
location-range="[[thread.locationRange]]"
- patch-num="[[patchNum]]"
+ patch-num="[[thread.patchNum]]"
path="[[path]]"
side="[[side]]"
project-config="[[projectConfig]]"></gr-diff-comment-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..ee4c5b3 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
@@ -23,14 +23,14 @@
type: Array,
value: function() { return []; },
},
- patchNum: String,
+ patchForNewThreads: String,
projectConfig: Object,
range: Object,
side: {
type: String,
value: 'REVISION',
},
- _threadGroups: {
+ _threads: {
type: Array,
value: function() { return []; },
},
@@ -40,17 +40,18 @@
'_commentsChanged(comments.*)',
],
- addNewThread: function(locationRange, commentSide) {
- this.push('_threadGroups', {
+ addNewThread: function(locationRange) {
+ this.push('_threads', {
comments: [],
locationRange: locationRange,
+ patchNum: this.patchForNewThreads,
});
},
removeThread: function(locationRange) {
- for (var i = 0; i < this._threadGroups.length; i++) {
- if (this._threadGroups[i].locationRange === locationRange) {
- this.splice('_threadGroups', i, 1);
+ for (var i = 0; i < this._threads.length; i++) {
+ if (this._threads[i].locationRange === locationRange) {
+ this.splice('_threads', i, 1);
return;
}
}
@@ -68,12 +69,20 @@
},
_commentsChanged: function() {
- this._threadGroups = this._getThreadGroups(this.comments);
+ this._threads = this._getThreadGroups(this.comments);
},
_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);
});
@@ -87,6 +96,16 @@
comment.__commentSide;
},
+ /**
+ * Determines what the patchNum of a thread should be. Use patchNum from
+ * comment if it exists, otherwise the property of the thread group.
+ * This is needed for switching between side-by-side and unified views when
+ * there are unsaved drafts.
+ */
+ _getPatchNum: function(comment) {
+ return comment.patchNum || this.patchForNewThreads;
+ },
+
_getThreadGroups: function(comments) {
var threadGroups = {};
@@ -106,6 +125,7 @@
comments: [comment],
locationRange: locationRange,
commentSide: comment.__commentSide,
+ patchNum: this._getPatchNum(comment),
};
}
}.bind(this));
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..bc08bab 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
@@ -49,6 +49,7 @@
});
test('_getThreadGroups', function() {
+ element.patchForNewThreads = 3;
var comments = [
{
id: 'sallys_confession',
@@ -79,12 +80,14 @@
__commentSide: 'left',
}],
locationRange: 'line-left',
+ patchNum: 3
},
];
assert.deepEqual(element._getThreadGroups(comments),
expectedThreadGroups);
+ // Patch num should get inherited from comment rather
comments.push({
id: 'betsys_confession',
message: 'i like you, jack',
@@ -113,6 +116,7 @@
updated: '2015-12-24 15:00:20.396000000',
__commentSide: 'left',
}],
+ patchNum: 3,
locationRange: 'line-left',
},
{
@@ -130,6 +134,7 @@
},
__commentSide: 'left',
}],
+ patchNum: 3,
locationRange: 'range-1-1-1-2-left',
},
];
@@ -165,6 +170,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() {
@@ -192,21 +223,33 @@
test('addNewThread', function() {
var locationRange = 'range-1-2-3-4';
- element._threadGroups = [{locationRange: 'line'}];
+ element._threads = [{locationRange: 'line'}];
element.addNewThread(locationRange);
- assert(element._threadGroups.length, 2);
+ assert(element._threads.length, 2);
+ });
+
+ test('_getPatchNum', function() {
+ element.patchForNewThreads = 3;
+ var comment = {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-23 15:00:20.396000000',
+ };
+ assert.equal(element._getPatchNum(comment), 3);
+ comment.patchNum = 4;
+ assert.equal(element._getPatchNum(comment), 4);
});
test('removeThread', function() {
var locationRange = 'range-1-2-3-4';
- element._threadGroups = [
+ element._threads = [
{locationRange: 'range-1-2-3-4', comments: []},
{locationRange: 'line', comments: []}
];
flushAsynchronousOperations();
element.removeThread(locationRange);
flushAsynchronousOperations();
- assert(element._threadGroups.length, 1);
+ assert(element._threads.length, 1);
});
});
</script>
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 f3ee6ac..c088b1a 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,16 +78,21 @@
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,
- lastComment ? lastComment.unresolved : undefined);
+ var range = opt_range ? opt_range :
+ lastComment ? lastComment.range : undefined;
+ var unresolved = lastComment ? lastComment.unresolved : undefined;
+ this.addDraft(opt_lineNum, range, unresolved);
}
},
@@ -103,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() {
@@ -192,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);
},
@@ -250,6 +271,7 @@
__draftID: Math.random().toString(36),
__date: new Date(),
path: this.path,
+ patchNum: this.patchNum,
side: this.side,
__commentSide: this.commentSide,
};
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 30c6fed..e0ae9ff 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',
@@ -599,8 +532,21 @@
test('_newDraft', function() {
element.commentSide = 'left';
+ element.patchNum = 3;
var draft = element._newDraft();
assert.equal(draft.__commentSide, 'left');
+ assert.equal(draft.patchNum, 3);
+ });
+
+ test('new comment gets created', function() {
+ element.comments = [];
+ element.addOrEditDraft(1);
+ assert.equal(element.comments.length, 1);
+ // Mock a submitted comment.
+ element.comments[0].id = element.comments[0].__draftID;
+ element.comments[0].__draft = false;
+ 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-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index eea27db..8d54b2a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -458,9 +458,10 @@
promises.push(this._getChangeDetail(this._changeNum));
- Promise.all(promises)
- .then(function() { return this.$.diff.reload(); }.bind(this))
- .then(function() { this._loading = false; }.bind(this));
+ Promise.all(promises).then(function() {
+ this._loading = false;
+ this.$.diff.reload();
+ }.bind(this));
this._loadCommentMap().then(function(commentMap) {
this._commentMap = commentMap;
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/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index d2cdd07..117ff24 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -236,15 +236,18 @@
sandbox.stub(element.$.diffBuilder, 'createCommentThreadGroup',
function() {
- return document.createElement('gr-diff-comment-thread-group');
+ var threadGroup =
+ document.createElement('gr-diff-comment-thread-group');
+ threadGroup.patchForNewThreads = 1;
+ return threadGroup;
});
// No thread groups.
assert.isNotOk(element._getThreadGroupForLine(contentEl));
// A thread group gets created.
- assert.isOk(element._getOrCreateThreadAtLineRange(contentEl, patchNum,
- commentSide, side));
+ assert.isOk(element._getOrCreateThreadAtLineRange(contentEl,
+ patchNum, commentSide, side));
// Try to fetch a thread with a different range.
range = {
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 6499f08..f9c2891 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -165,5 +165,5 @@
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting"></gr-reporting>
</template>
- <script src="gr-app.js"></script>
+ <script src="gr-app.js" crossorigin="anonymous"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
index e675de2..dcda26c 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -42,9 +42,6 @@
stub('gr-rest-api-interface', {
getAccount: function() { return Promise.resolve(account); },
- getAccountHttpPassword: function() {
- return Promise.resolve(password);
- },
});
element = fixture('basic');
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index fbfdcc3..0d381cb 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -123,7 +123,6 @@
getAccountEmails: function() { return Promise.resolve(); },
getConfig: function() { return Promise.resolve(config); },
getAccountGroups: function() { return Promise.resolve([]); },
- getAccountHttpPassword: function() { return Promise.resolve(''); },
});
element = fixture('basic');
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-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 4d64a77..3d7a4d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -968,11 +968,6 @@
'/topic', {topic: topic});
},
- getAccountHttpPassword: function(opt_errFn) {
- return this._fetchSharedCacheURL('/accounts/self/password.http',
- opt_errFn);
- },
-
deleteAccountHttpPassword: function() {
return this.send('DELETE', '/accounts/self/password.http');
},
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index 05f6119..db9a1c5 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -29,7 +29,7 @@
<link rel="stylesheet" href="/styles/fonts.css">
<link rel="stylesheet" href="/styles/main.css">
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<link rel="preload" href="/elements/gr-app.js" crossorigin="anonymous">
+<link rel="preload" href="/elements/gr-app.js" as="script" crossorigin="anonymous">
<link rel="import" href="/elements/gr-app.html">
<body unresolved>
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 6e5faf0..de82af8 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -30,6 +30,7 @@
resources = resources,
deps = provided_deps + deps + GWT_PLUGIN_DEPS_NEVERLINK + PLUGIN_DEPS_NEVERLINK,
visibility = ['//visibility:public'],
+ **kwargs
)
static_jars = []
@@ -56,6 +57,7 @@
resources = list(set(srcs + resources)),
runtime_deps = deps + GWT_PLUGIN_DEPS,
visibility = ['//visibility:public'],
+ **kwargs
)
genrule2(
name = '%s-static' % name,
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=\#